Die YouTube API

Die vollständige Dokumentation der YouTube API findet ihr unter
https://developers.google.com/youtube/v3/docs

Vorteile der YouTube API

Vorteile gegenüber Webscraping

Nachteile

Die YouTube API mit R

Zugriff auf die YouTube API

Für den Zugriff auf die YouTube API verwenden wir das R-Paket tuber. Dieses erlaubt es uns, komfortabel YoutTube Daten zu ziehen und in einem für die weitere Verarbeitung geeigneten Format auszugeben. Dazu müssen wir zunächst das Paket installieren. Dabei gibt es jedoch ein Problem: seit dem letzten offiziellen Release des Pakets hat sich etwas an der YouTube API geändert und die Funktion, welche es erlaubt, die vorgeschlagenen Videos zu ziehen, funktioniert nicht mehr. Zum Glück ist das in der aktuellen Entwicklerversion des Pakets, die wir auf Github finden können, schon behoben. Das heißt allerdings, dass wir nicht wie gewohnt install.packages() verwenden können, sondern das Paket direkt von Github installieren müssen. Dazu benötigen wir die Funktion install_git() aus der devtools Library. Diese müssen wir zunächst installieren. mit devtools::install_git() können wir dann auf die benötigte Funktion zugreifen, ohne gleich die ganze Library laden zu müssen (wir brauchen nur einmal diese eine Funktion).

install.packages("devtools")

devtools::install_github("soodoku/tuber", build_vignettes = TRUE) 

Nachdem wir die aktuelle Version von tuber installiert haben, können wir sie wie gewohnt laden. Außerdem werden wir wieder das Tidyverse und lubridate verwenden, die wir ebenfalls laden.

library(tuber)
library(tidyverse)
library(lubridate)

Nun müssen wir zunächst eine sogenannte Authentifizierung durchführen, die dafür sorgt, dass die API unseren Zugang anerkennt. Dafür benötigen wir die App ID und das App Secret, die wir bereits letzte Woche erstellt haben. Fügt diese dazu einfach in den untenstehenden Code ein und führt ihn aus.

Ihr werdet zunächst gefragt, ob ihr eine lokale Datei erstellen wollt, um die Credentials zu speichern. Das ist komfortabel, da ihr den folgenden Prozess nicht jedesmal neu durchlaufen müsst, allerdings auch etwas fehleranfällig. Falls es ein Problem mit eurer Authentifizierung gíbt, müsst ihr die Datei “.httr-oauth” händisch löschen und die Funktion erneut ausführen. Ich würde empfehlen, hier “No” auszuwählen.

Danach öffnet sich euer Browser und ihr landet auf einer Authentifizierungsseite. Dort müsst ihr euch mit dem Account einloggen, den ihr als Test Account in der App angegeben habt. Danach stimmt ihr zu, dass ihr der Entwickler*in (euch selbst) vertraut und Zugriff auf die API gewährt. Und fertig!

yt_oauth(
  app_id = "xxxxxxxx",
  app_secret = "xxxxxxxx")

Zugriff auf Metadaten von Videos

Testen wir zunächst anhand eines einzelnen Videos, ob unser Zugriff funktioniert. Das geht einfach mit der Funktion get_video_details(), die uns für eine Video ID die spezifizierten Metadaten zurückgibt. Die ID eines Videos findet ihr, wenn ihr ein Video auf YouTube aufruft, in der Adresszeile eures Browsers nach dem Schema: https://www.youtube.com/watch?v=VIDEO_ID

Wir wollen beispielsweise für die Regierungserklärung von Kanzler Scholz, das sogenannte "snippet", also die Kurzinformationen zu Titel, Publikationsdatum, Beschreibung etc. Weitere Informationen zu den verfügbaren Daten findet ihr unter ?get_video_details(). Außerdem können wir mit as.data.frame = TRUE spezifizieren, dass wir die Daten gerne als Dataframe hätten, bei dem eine Spalte einer Variable entspricht.

video_data <- get_video_details(video_id = "6qSVM-APauo",
                                part = "snippet",
                                as.data.frame = TRUE)

Auf dieselbe Art und Weise können wir auch für mehrere Videos auf einmal Daten ziehen, indem wir einen Vector aus den IDs machen. Dann ist jede Reihe im Dataframe ein Video.

video_data <- get_video_details(video_id = c("6qSVM-APauo", 
                                             "Q5z9QLrSLfQ", 
                                             "QyzZvzD8gD4"),
                                part = "snippet",
                                as.data.frame = TRUE)

Zugriff auf Kommentare

Um auf Kommentare für einzelne Videos zuzugreifen, können wir die Funktion get_all_comments() verwenden, welche lediglich die Video ID benötigt. Beachtet, dass get_all_comments() allerdings nicht alle Antworten auf Kommentare ausgibt.

comments <- get_all_comments(video_id = "6qSVM-APauo")

get_all_comments() kann jedoch immer nur die Kommentare für ein Video auf einmal ziehen. Wenn wir die Kommentare für mehrere Videos wollen, können wir dies entweder erreichen, indem wir die Funktion für jede Video ID einzeln ausführen. Eleganter ist es jedoch, einen for-loop zu verwenden. Dieser erlaubt es, in einer “Schleife” (einem Loop) durch alle Videos durchzugehen und die Kommentare der Videos jeweils mit get_all_comments() zu ziehen. Wir gehen also zwischen Reihe 1 und der Gesamtzahl der Reihen im Datensatz (nrow(video_data)) durch jeden Eintrag und füllen in den folgenden Operationen und Funktionen den Indikator [i] ein, welche je die aktuelle Reihe angibt. Diese einzelnen Dataframes, die so in jeder Runde des Loops erstellt werden, werden dann einem (anfangs leeren) Dataframe bzw. Tibble rundenweise hinzugefügt. Am Ende des Loops bauen wir mit Sys.sleep() eine kleine Pause ein. So “schläft” das System je eine Sekunde, bevor es mit dem nächsten Kanal weitergeht. Diese Pausen beugen Fehlermeldungen von APIs vor, die auftreten können, wenn wir zu schnell zu viele Anfragen stellen. In diesem Beispiel wollen die Kommentare von allen drei Videos erhalten, für die wir bereits die Metadaten gezogen haben.

video_comments <- tibble() # leerer Container

for (i in 1:nrow(video_data)) {
  
  comments_per_video <- get_all_comments(video_data$id[i]) # API Call
  
  video_comments <-  bind_rows(video_comments, comments_per_video) # zusammenführen
  
  Sys.sleep(1) # Pausieren
}

Wir können genauso gut durch einen Vector loopen. In diesem Falle können wir statt nrow() die Funktion length() verwenden (ein Vektor hat keine Reihen, aber eine Länge, die durch die Anzahl der einzelnen Elemente bestimmt wird).

ids <- c("6qSVM-APauo", # Vektor mit IDs
         "Q5z9QLrSLfQ",
         "QyzZvzD8gD4")

video_comments <- tibble() # leerer Container

for (i in 1:length(ids)) {
  
  comments_per_video <- get_all_comments(ids[i]) # API Call
  
  video_comments <-  bind_rows(video_comments, comments_per_video) # zusammenführen
  
  Sys.sleep(1) # Pausieren
}

Ein wichtiger Hinweis zu for-loops in R: im Gegensatz zu Python, das viele dieser und ähnlicher Loops verwendet, sind Loops in R nur selten eine gute Idee. Das liegt daran, dass sie, insbesondere für große Datensätze, schlecht skalieren und somit schnell sehr langsam und ineffizient werden. Daher sollten wir in R wann immer möglich Funktionen verwnden, die direkt auf dem Datensatz operieren, etwa aus dem Tidyverse. Dies macht es einfach, for-loops zu vermeiden. Dennoch gibt es eine begrenzte Anzahl an Fällen, in denen for-loops sinnvoll sind. Dies ist insbesondere der Fall bei API Calls, da wir bspws. mit Sys.sleep() kontrollieren können, wieviel Zeit zwischen den einzelnen Anfragen vergeht. Wenn wir alle Anfragen auf einmal stellen, kann es schnell passieren, dass unser Zugriff temporär gesperrt wird, um die API vor Überlastung zu schützen. Deshalb ist es oft sinnvoll, diese Art von Operationen nacheinander, eben in einem Loop, auszuführen.

Vorgeschlagene Videos

Die Funktion get_related_videos() gibt uns die vorgeschlagenen Videos zu einem Video aus. Mit max_results geben wir die Anzahl an vorgeschlagenen Videos an, die wir pro Video erhalten möchten - bis zu 50 Stück. Es ist allerdings anzunehmen, dass nur weniger User*innen mehr als 10 vorgeschlagene Videos überhaupt sehen. Wer scrollt schon lange durch diese Liste? Außerdem belastet es unser API Ratelimit relativ stark, die Daten dieser vogeschlagenen Videos zu ziehen (mehr dazu in der API Dokumentation). Deshalb entscheiden wir uns, nur 10 vorgeschlagene Videos zu ziehen:

related_videos <- get_related_videos("6qSVM-APauo",
                                     max_results = 10)

Wenn wir die vorgeschlagenen Videos für mehr als ein Video ziehen wollen, müssen wir erneut einen for-loop verwenden. Die Video ID gibt dabei an, welches das “originale” Video ist, von dem wir ausgegangen sind. Da get_related_videos() uns automatisch einen kurzen Text dazu ausgibt, wieviele Videos gefunden wurden, benutzen wir diesmal außerdem cat() um in jeder Schleife ebenfalls einen kleinen Printout hinzuzufügen. Dieser besteht aus der ID des aktuellen Videos und einem Absatz. Somit können wir immer sehen, zu welchem Video wieviele Ergebnisse gefunden wurden.


ids <- c("6qSVM-APauo", # Vektor mit IDs
         "Q5z9QLrSLfQ",
         "QyzZvzD8gD4")

more_related_videos <- tibble()

for (i in 1:length(ids)) {
  
  cat(ids[i], "\n")
  
  try({
    
    related_videos_per_video <- get_related_videos(ids[i], max_results = 10) 
    
    more_related_videos <- bind_rows(more_related_videos, related_videos_per_video)
    
    Sys.sleep(1)
    
  })
  
}

Zugriff auf alle Videos eines Kanals

Wollen wir die Metadaten aller Videos eines Channels auf einmal ziehen, brauchen wir zunächst die Channel ID. Diese ist leider nicht ganz so leicht zu finden wie die Video ID. Wenn ihr mit dem Browser auf die YouTube Seite eines Kanals geht, müsst ihr euch den Seitenquelltext anzeigen lassen (mit Firefox bspws. via Rechtsklick). Daraufhin seht ihr den HTML-Code der Seite. Darin versteckt sich die Channel ID: sucht mit str+f nach “channel_id=”. Daraufhin solltet ihr einen Abschnitt im Code entdecken, der euch die Channel ID verrät, bspws. channel_id=UC7TAA2WYlPfb6eDJCeX4u0w für die Grünen.

Diese Channel ID können wir dann mit der Funktion list_channel_videos() alle Videos eines Channels aus, bis zu der maximalen Anzahl die wir mit max_results festlegen. Wir können Inf (für Infinite, also unendlich) angeben, um alle Ergebnisse zu erhalten.

gruene_videos <- list_channel_videos(channel_id = "UC7TAA2WYlPfb6eDJCeX4u0w",
                                     max_results = Inf)

Wir bekommen die Videos erneut in einem praktischen Dataframe, wenn auch mit etwas weniger Informationen als vorher. Da wir jedoch die video IDs aller Videos erhalten haben, könnten wir diese erneut an get_video_details() geben, um mehr Informationen zu erhalten. Ich mache dies einmal testweise für die ersten 5 Einträge in der Spalte contentDetails.videoID aus unserem Dataframe mit grünen Video. Dazu verwende ich eine sogenannten Index, welcher zwischen den eckigen Klammern steht: [1:5] wählt die Einträge 1 bis 5 aus, [5] den fünften Eintrag, ein weglassen des Index wählt alle Einträge aus. $ spezifiziert die Spalte eines Dataframes.

some_video_data <- get_video_details(video_id = gruene_videos$contentDetails.videoId[1:5],
                                     part = "snippet",
                                     as.data.frame = TRUE)

Um einen Überblick über die veröffentlichten Videos zu erhalten, können wir nun wieder das Tidyverse und ggplot verwenden, um Daten zu manipulieren und zu visulisieren. Es gibt jedoch ein Problem: Die Variabale contentDetails.publishedAt, also der Publikationszeitpunkt, ist eine Character-Variable (also einfach Zeichen). Das kann später Probleme machen, da viele Funktionen eine Variable im Datums-Format (POSIXct) erwarten. Mit mutate() können wir das allerdings leicht beheben. Die lubridate-Funktion ymd_hms() wandelt den Character String jeder Reihe in einen Datums-String nach dem Format Year-Month-Day_Hour-Minute-Second um:

gruene_videos <- gruene_videos %>% 
  mutate(contentDetails.videoPublishedAt = ymd_hms(contentDetails.videoPublishedAt))

Jetzt können wir beispielsweise einfach die Videos über Zeit plotten, indem wir sie zunächst mit summarise()wöchentlich akkumulieren:

gruene_videos %>% 
  mutate(week = floor_date(contentDetails.videoPublishedAt, unit = "week")) %>% 
  summarise(videos = n(), .by = week) %>% 
  ggplot(aes(x = week, y = videos)) +
  geom_line()

Übungen

Ihr seid dran: Wiederholt die obigen Schritte für Videos, die für eure Forschungsfrage relevant sind. Falls ihr die Metadaten oder IDs aller Videos mehrerer Kanäle ziehen wollt, findet ihr unten Beispielcode. Dort findet ihr auch Hinweise und Code, um die in der Einführungspräsentation vorgestellten Fallbeispiele zu replizieren.

1. Metadaten

Zieht euch die Metadaten eines oder mehrerer Videos.

2. Kommentare

Zieht euch die Kommentare für die Videos, die ihr für 1. bereits nachgeschlagen habt.

3. Vorgeschlagene Videos

Zieht die ersten 10 vorgeschlagenen Videos für eure Videos. Hinweis: Wenn ihr sehr viele Videos nachschlagt, kann dies zum Erreichen des Ratelimits führen. Zum Umgang damit findet ihr am Ende dieses Dokuments (Abschniit “Vorgeschlagene Videos zu Parteivideos”) Informationen. Für Übungszwecke reicht es, wenn ihr für maximal 10 Videos die vorgeschlagenen Videos zieht.

4. Analyse

Analysiert eure Ergebnisse:

  1. Lasst euch die Anzahl an Kommentare pro Video mit summarise() zusammenfassen
  2. Visualisiert die Menge an Kommentaren über Zeit (ggf. pro Video)
  3. Lasst euch das durschnittliche Publikationsdatum der vorgeschlagenen Videos pro Ausgangsvideo ausgeben

Replikation der Fallbeispiele

Fallbeispiel: Youtube Kanäle der Parteien

Das Fallbeispiel aus der Präsentation sind die YouTube Kanäle der großen deutschen Parteien . Für eure eigenen Projekte könnt ihr natürlich alle anderen Kanäle verwenden. In diesem Abschnitt findet ihr Code, um das Beispiel zu reproduzieren oder (mit leichten Änderungen) auf andere Kanäle anzuwenden.

Wenn wir die Videos mehrerer Kanäle ziehen wollen, sollten wir zunächst die Channel IDs (händisch) heraussuchen und ablegen. Da ich außerdem vermerken will, welche Partei zu welchem Channel gehört, lege ich dazu eine tibble, also eine Art Dataframe, an:

party_channels <- tibble(
  party = c("cdu",
            "spd",
            "gruene",
            "fdp", 
            "afd", 
            "linke"),
  channel_id = c("UCKyWIEse3u7ExKfAWuDMVnw",
                 "UCSmbK1WtpYn2sOGLvSSXkKw",
                 "UC7TAA2WYlPfb6eDJCeX4u0w",
                 "UC-sMkrfoQDH-xzMxPNckGFw",
                 "UCq2rogaxLtQFrYG3X3KYNww",
                 "UCA95T5bSGxNOAODBdbR2rYQ")
)

Um jetzt auf alle Kanäle zuzugreifen, kann ich einen sogannten for-loop verwenden. Dieser erlaubt es, in einer “Schleife” (einem Loop) durch alle Kanäle durchzugehen und die Metadaten der Videos jeweils mit list_channel_videos() zu ziehen. Wir gehen also zwischen Reihe 1 und der Gesamtzahl der Reihen im Datensatz (nrow(party_channels)) durch jeden Eintrag und füllen in den folgenden Operationen und Funktionen den Indikator [i] ein, welche je die aktuelle Reihe angibt. So fügen wir außerdem den Partei-Indikator der aktuellen Reihe zu unserem Ergebnis hinzu. Diese einzelnen Dataframes, die so in jeder Runde des Loops erstellt werden, werden dann einem (anfangs leeren) Dataframe bzw. Tibble rundenweise hinzugefügt. Außerdem nutzen wir cat() für einen kleinen Printout, der uns verrät, für welche Partei wir wie viele Videos erhalten. Das ist aber nicht unbedingt nötig. Am Ende des Loops bauen wir mit Sys.sleep() eine kleine Pause ein. So “schläft” das System je eine Sekunde, bevor es mit dem nächsten Kanal weitergeht. Diese Pausen beugen Fehlermeldungen von APIs vor, die auftreten können, wenn wir zu schnell zu viele Anfragen stellen.

Ein wichtiger Hinweis zu for-loops in R: im Gegensatz zu Python, das viele dieser und ähnlicher Loops verwendet, sind Loops in R nur selten eine gute Idee. Das liegt daran, dass sie, insbesondere für große Datensätze, schlecht skalieren und somit schnell sehr langsam und ineffizient werden. Daher sollten wir in R wann immer möglich Funktionen verwnden, die direkt auf dem Datensatz operieren, etwa aus dem Tidyverse. Dies macht es einfach, for-loops zu vermeiden. Dennoch gibt es eine begrenzte Anzahl an Fällen, in denen for-loops sinnvoll sind. Dies ist insbesondere der Fall bei API Calls, da wir bspws. mit Sys.sleep() kontrollieren können, wieviel Zeit zwischen den einzelnen Anfragen vergeht. Wenn wir alle Anfragen auf einmal stellen, kann es schnell passieren, dass unser Zugriff temporär gesperrt wird, um die API vor Überlastung zu schützen. Deshalb ist es oft sinnvoll, diese Art von Operationen nacheinander, eben in einem Loop, auszuführen.

party_videos <- tibble() # empty container object to fill

# loop through every row in the dataframe of party channels
for (i in 1:nrow(party_channels)) { 
  # make the API call
  videos <-  list_channel_videos(
    channel_id = party_channels$channel_id[i],
    max_results = Inf)
  # add party indicator
  videos <- videos %>% mutate(party = party_channels$party[i]) 
  # bind results
  party_videos <- bind_rows(party_videos, videos) 
  # some printout to keep track
  cat(party_channels$party[i], ": ", 
      nrow(videos), 
      " videos retrieved \n", 
      sep = "") 
  # a sleep period between calls to avoid API errors
  Sys.sleep(1) 
  
}

Erneut wollen wir die Variable contentDetails.videoPublishedAt in ein richtiges Datum-Format umwandeln. Außerdem können wir mit distinct() sicherstellen, dass wir keine Duplikate in unseren Daten haben, indem wir nach einzigarten Video IDs filtern. Außerdem wollen wir noch ein “Label” für die Parteien, also einen Parteiindikator für grafische Darstellungen etc., der den kompletten (und korrekt geschriebenen) Parteinamen enthält. Dieses “data cleaning” ist ein wichtiger Bestandteil der Datenverarbeitung, der dafür sorgt, dass wir für unsere Analysen saubere Daten zur Verfügung haben. Nur so können wir gute Resultate erhalten!

party_videos <- party_videos %>% 
  mutate(contentDetails.videoPublishedAt = ymd_hms(contentDetails.videoPublishedAt)) %>%  
  distinct(contentDetails.videoId, .keep_all = T) %>% 
  mutate(party_label = case_when( 
    party == "afd" ~ "AfD",
    party == "cdu" ~ "CDU",
    party == "spd" ~ "SPD",
    party == "fdp" ~ "FDP",
    party == "gruene" ~ "Bündnis 90/Die Grünen",
    party == "linke" ~ "DIE LINKE"
  ))

Nachdem wir diese Schritte durchgeführt haben, haben wir denselben party_videos Datensatz produziert, den ihr auch im Learnweb findet. In der vorherigen Übung habt ihr bereits den Code kennengelernt, der uns eine hübsche Übersicht über die Parteivideos über Zeit generiert:

colors <-
  c(
    "AfD" = rgb(0, 60, 145, maxColorValue = 255),
    "CDU" = rgb(50, 48, 46, maxColorValue = 255),
    "DIE LINKE" = rgb(182, 28, 62, maxColorValue = 255),
    "FDP" = rgb(255, 237, 0, maxColorValue = 255),
    "Bündnis 90/Die Grünen" = rgb(70, 150, 43, maxColorValue = 255),
    "SPD" = rgb(227, 0, 15, maxColorValue = 255)
  )

party_videos %>% 
  mutate(month = floor_date(contentDetails.videoPublishedAt, unit = "month")) %>% 
  summarise(videos = n(), .by = c(month, party_label)) %>% 
  ggplot(aes(x = month, y = videos, color = party_label)) +
  geom_line() +
  facet_wrap( ~ party_label, ncol = 3) +
  guides(color = "none") +
  scale_color_manual(values = colors) +
  labs(
    x = "Monat",
    y = "Veröffentlichte Videos",
    color = "Partei",
    title = "Videos über Zeit nach Partei, Gesamtzeitraum"
  ) +
  theme_bw()

Kommentare zu Parteivideos

Wenn wir nun die Kommentare der Parteivideos ziehen wollen, sollten wir zuerst den Beobachtungszeitraum einschränken. Ansonsten kann es leicht passieren, dass wir, wenn wir für alle 13.512 Videos Metadaten ziehen, an das Rate Limit der API geraten und einen Tag warten müssen, bevor wir weiterarbeiten können (zum Umgang damit später mehr). Deshalb wollen wir uns exemplarisch auf die im Januar 2022 veröffentlichen Videos beschränken. Dazu schränken wir zunächst mit filter() den Datensatz ein:

videos_january <- party_videos %>% 
  filter(contentDetails.videoPublishedAt >= ymd("2022-01-01") & 
         contentDetails.videoPublishedAt < ymd("2022-02-01")) 

Daraufhin gehen wir ähnlich vor wie für die Kanäle, indem wir einen for-loop verwenden:

comments_january <- tibble() # empty container

for (i in videos_january$contentDetails.videoId) { 
  # note how we can loop directly over the contents of this list, rather than using i in 1:nrow(videos_january) and accessing the content later through videos_january$contentDetails.videoId[i]
  
  comments <- NULL # this is only necessary to indicate missing / erroneous API calls correctly (otherwise we could just overwrite it every loop)
  
  cat(i, "\n")
  
  
  try({  # wrapping the call in try() makes sure the loop continues when an API call fails. Wrapping bind_rows() as well _after_ the call makes sure rows are only bound if comments are retrieved
    
      comments <- get_all_comments(video_id = i) # make API call
    
      comments_january <- bind_rows(comments_january, comments) # bind results
      
  })
  
  if(!is.null(comments)) {cat(nrow(comments), "comments retrieved\n")} # some output to keep track
  else {cat("no results \n")}
    
  Sys.sleep(1) # a sleep period between calls to avoid API errors
}

Jetzt fehlt uns allerdings ein Indikator, welches Video (und damit welche Kommentare) zu welcher Partei gehört. Einen solchen Indikator konnten wir auch nicht im Loop selbst erstellen, da wir diesmal nicht über die Reihen, sondern über die Werte einer Variable geloopt haben. Mit der Funktion left_join() können wir allerdings einfach die Werte aus einem anderen Datensatz hizufügen. Bei dieser Gelegenheit nehmen wir außerdem noch ein wenig Datacleaning vor:

party_comments_january <- 
  left_join(videos_january, 
            comments_january %>% # join the comments on the video list...
              select(videoId, textDisplay, textOriginal, authorDisplayName, 
                     likeCount, publishedAt, id) %>%  # ... but select relevant data first.. 
              rename(comment_id = id),    # ... and rename the ID variable for clarity
            # we need to specify that the IDs have different column names in the two datasets
            by = join_by(contentDetails.videoId == videoId), 
            multiple = "all") %>%  # we expect multiple matches from the right-hand side (comments_january) to the left-hand side (videos_january)
  mutate(likeCount = as.integer(likeCount),              # count as integer, not character
         publishedAt = ymd_hms(publishedAt)) %>%       # proper dateteime format
  rename(video_id = contentDetails.videoId,              # rename some variables for clarity
         video_PublishedAt = contentDetails.videoPublishedAt,
         comment_PublishedAt = publishedAt) %>% 
  select(!c(kind, etag, id))   # drop some unnecessary variables

Jetzt können wir uns mit etwas zusätzlichem Datawrangling eine Überblicksstatistik ausgeben lassen:

party_comments_january %>% group_by(party_label) %>% 
  # count non-missing and missing comments
  count(!is.na(comment_id), .drop = F) %>% 
  # wrangle into wide format
  pivot_wider(names_from = `!is.na(comment_id)`, values_from = n) %>%   
   # rename variables for clarity
  rename(comments = "TRUE", videos_without_comments = "FALSE") %>%  
  # replace NA values with 0
  replace_na(list(comments = 0, videos_without_comments = 0)) %>%        
  left_join(videos_january %>% group_by(party_label) %>% summarise(total_videos = n()), by = "party_label") %>%  # add total number of videos through another dataframe
  mutate(average_comments_per_video = comments / total_videos)   # get relative comments

Vorgeschlagene Videos zu Parteivideos

Um die vorgeschlagenen Videos zu den Parteivideos aus dem Januar 2022 zu erhalten, können wir analog zu dem Beispiel oben vorgehen. Da wir jedoch relativ viele Videos nachschauen wollen, können wir an das Ratelimit der API stoßen. In diesem Fall würden wir das Ergebnis abspeichern und, nachdem das Ratelimit zurückgesetzt wurde (idR. am nächsten Tag), die verbliebenen Videos nachschlagen:

related_videos_january <- tibble()

for (id in videos_january$contentDetails.videoId) {
  
  cat(id, "\n")
  
  try({
    
    related_videos <- get_related_videos(id, max_results = 10) # top 10 - we can assume most people won't scroll further, and this is less taxing on the rate limits than e.g. 50
    
    related_videos_january <- bind_rows(related_videos_january, related_videos)
    
    Sys.sleep(1)
    
  })
  
}

saveRDS(related_videos_january, "related_videos_january_pt1.RDS")

# if we hit an API rate limit, we have to wait for it to refresh and resume the collection

related_videos_january <- readRDS("related_videos_january_pt1.RDS")

missing_related_videos <- videos_january %>% anti_join(related_videos_january, by = join_by(contentDetails.videoId == video_id)) # an anti_join is an easy way to return all videos without a match in the related_video data


for (id in missing_related_videos$contentDetails.videoId) {
  
  cat(id, "\n")
  
  try({
    
    related_videos <- get_related_videos(id, max_results = 10) # top 10 - we can assume most people won't scroll further, and this is less taxing on the rate limits than e.g. 50
    
    related_videos_january <- bind_rows(related_videos_january, related_videos)
    
    Sys.sleep(1)
    
  })
  
}

saveRDS(related_videos_january, "related_videos_january.RDS")
LS0tDQp0aXRsZTogIlIgJiBkaWUgWW91VHViZSBBUEkiDQpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sNCi0tLQ0KDQojIyBEaWUgWW91VHViZSBBUEkNCg0KWyFbXSh5dC1kYXRhLWFwaS1zZWFyY2hfNzIwLnBuZyldKGh0dHBzOi8vZGV2ZWxvcGVycy5nb29nbGUuY29tL3N0YXRpYy95b3V0dWJlL2ltYWdlcy95dC1kYXRhLWFwaS1zZWFyY2hfNzIwLnBuZz9obD1kZSkNCg0KRGllICoqdm9sbHN0w6RuZGlnZSBEb2t1bWVudGF0aW9uIGRlciBZb3VUdWJlIEFQSSoqIGZpbmRldCBpaHIgdW50ZXJcDQo8aHR0cHM6Ly9kZXZlbG9wZXJzLmdvb2dsZS5jb20veW91dHViZS92My9kb2NzPg0KDQojIyBWb3J0ZWlsZSBkZXIgWW91VHViZSBBUEkNCg0KKipWb3J0ZWlsZSBnZWdlbsO8YmVyIFdlYnNjcmFwaW5nKioNCg0KLSAgIHNhdWJlciBmb3JtYXRpZXJ0ZSwgdmVybMOkc3NsaWNoZSBEYXRlbg0KLSAgIGxlZ2FsZSwgZHVyY2ggVGVybXMgb2YgU2VydmljZSByZWd1bGllcnRlIERhdGVuYmVzY2hhZmZ1bmcNCi0gICBndXRlIERva3VtZW50YXRpb24NCi0gICAqa2VpbmUgSFRNTC1LZW5udG5pc3NlIG7DtnRpZyoNCi0gICBSLVBhY2thZ2VzIHp1bSBVbWdhbmcgbWl0IGRlciBBUEksIHouQi4gdHViZXINCg0KKipOYWNodGVpbGUqKg0KDQotICAgTcO2Z2xpY2hlIERhdGVuIGR1cmNoIGRpZSBFbmRwb2ludHMgZGVyIEFQSSB2b3JnZWdlYmVuDQotICAgUmF0ZSBMaW1pdHMgcmVnbGVtZW50aWVyZW4gZGllIHTDpGdsaWNoIHZlcmbDvGdiYXJlIERhdGVubWVuZ2UNCi0gICBBdXRoZW50aWZpemllcnVuZyB2aWEgT0F1dGggMi4wIC8gQVBJIEtleXMgZXR3YXMgdW1zdMOkbmRsaWNoDQoNCiMgRGllIFlvdVR1YmUgQVBJIG1pdCAqUioNCg0KIyMgWnVncmlmZiBhdWYgZGllIFlvdVR1YmUgQVBJDQoNCkbDvHIgZGVuIFp1Z3JpZmYgYXVmIGRpZSBZb3VUdWJlIEFQSSB2ZXJ3ZW5kZW4gd2lyIGRhcyAqUiotUGFrZXQgKnR1YmVyKi4gRGllc2VzIGVybGF1YnQgZXMgdW5zLCBrb21mb3J0YWJlbCBZb3V0VHViZSBEYXRlbiB6dSB6aWVoZW4gdW5kIGluIGVpbmVtIGbDvHIgZGllIHdlaXRlcmUgVmVyYXJiZWl0dW5nIGdlZWlnbmV0ZW4gRm9ybWF0IGF1c3p1Z2ViZW4uIERhenUgbcO8c3NlbiB3aXIgenVuw6RjaHN0IGRhcyBQYWtldCBpbnN0YWxsaWVyZW4uIERhYmVpIGdpYnQgZXMgamVkb2NoIGVpbiBQcm9ibGVtOiBzZWl0IGRlbSBsZXR6dGVuIG9mZml6aWVsbGVuIFJlbGVhc2UgZGVzIFBha2V0cyBoYXQgc2ljaCBldHdhcyBhbiBkZXIgWW91VHViZSBBUEkgZ2XDpG5kZXJ0IHVuZCBkaWUgRnVua3Rpb24sIHdlbGNoZSBlcyBlcmxhdWJ0LCBkaWUgdm9yZ2VzY2hsYWdlbmVuIFZpZGVvcyB6dSB6aWVoZW4sIGZ1bmt0aW9uaWVydCBuaWNodCBtZWhyLiBadW0gR2zDvGNrIGlzdCBkYXMgaW4gZGVyIGFrdHVlbGxlbiBFbnR3aWNrbGVydmVyc2lvbiBkZXMgUGFrZXRzLCBkaWUgd2lyIGF1ZiBHaXRodWIgZmluZGVuIGvDtm5uZW4sIHNjaG9uIGJlaG9iZW4uIERhcyBoZWnDn3QgYWxsZXJkaW5ncywgZGFzcyB3aXIgbmljaHQgd2llIGdld29obnQgYGluc3RhbGwucGFja2FnZXMoKWAgdmVyd2VuZGVuIGvDtm5uZW4sIHNvbmRlcm4gZGFzIFBha2V0IGRpcmVrdCB2b24gR2l0aHViIGluc3RhbGxpZXJlbiBtw7xzc2VuLiBEYXp1IGJlbsO2dGlnZW4gd2lyIGRpZSBGdW5rdGlvbiBgaW5zdGFsbF9naXQoKWAgYXVzIGRlciAqZGV2dG9vbHMqIExpYnJhcnkuIERpZXNlIG3DvHNzZW4gd2lyIHp1bsOkY2hzdCBpbnN0YWxsaWVyZW4uIG1pdCBgZGV2dG9vbHM6Omluc3RhbGxfZ2l0KClgIGvDtm5uZW4gd2lyIGRhbm4gYXVmIGRpZSBiZW7DtnRpZ3RlIEZ1bmt0aW9uIHp1Z3JlaWZlbiwgb2huZSBnbGVpY2ggZGllIGdhbnplIExpYnJhcnkgbGFkZW4genUgbcO8c3NlbiAod2lyIGJyYXVjaGVuIG51ciBlaW5tYWwgZGllc2UgZWluZSBGdW5rdGlvbikuDQoNCmBgYHtyfQ0KaW5zdGFsbC5wYWNrYWdlcygiZGV2dG9vbHMiKQ0KDQpkZXZ0b29sczo6aW5zdGFsbF9naXRodWIoInNvb2Rva3UvdHViZXIiLCBidWlsZF92aWduZXR0ZXMgPSBUUlVFKSANCmBgYA0KDQpOYWNoZGVtIHdpciBkaWUgYWt0dWVsbGUgVmVyc2lvbiB2b24gKnR1YmVyKiBpbnN0YWxsaWVydCBoYWJlbiwga8O2bm5lbiB3aXIgc2llIHdpZSBnZXdvaG50IGxhZGVuLiBBdcOfZXJkZW0gd2VyZGVuIHdpciB3aWVkZXIgZGFzICpUaWR5dmVyc2UqIHVuZCAqbHVicmlkYXRlKiB2ZXJ3ZW5kZW4sIGRpZSB3aXIgZWJlbmZhbGxzIGxhZGVuLg0KDQpgYGB7cn0NCmxpYnJhcnkodHViZXIpDQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmxpYnJhcnkobHVicmlkYXRlKQ0KYGBgDQoNCk51biBtw7xzc2VuIHdpciB6dW7DpGNoc3QgZWluZSBzb2dlbmFubnRlIEF1dGhlbnRpZml6aWVydW5nIGR1cmNoZsO8aHJlbiwgZGllIGRhZsO8ciBzb3JndCwgZGFzcyBkaWUgQVBJIHVuc2VyZW4gWnVnYW5nIGFuZXJrZW5udC4gRGFmw7xyIGJlbsO2dGlnZW4gd2lyIGRpZSAqKkFwcCBJRCoqIHVuZCBkYXMgKipBcHAgU2VjcmV0KiosIGRpZSB3aXIgYmVyZWl0cyBsZXR6dGUgV29jaGUgZXJzdGVsbHQgaGFiZW4uIEbDvGd0IGRpZXNlIGRhenUgZWluZmFjaCBpbiBkZW4gdW50ZW5zdGVoZW5kZW4gQ29kZSBlaW4gdW5kIGbDvGhydCBpaG4gYXVzLg0KDQpJaHIgd2VyZGV0IHp1bsOkY2hzdCBnZWZyYWd0LCBvYiBpaHIgZWluZSBsb2thbGUgRGF0ZWkgZXJzdGVsbGVuIHdvbGx0LCB1bSBkaWUgQ3JlZGVudGlhbHMgenUgc3BlaWNoZXJuLiBEYXMgaXN0IGtvbWZvcnRhYmVsLCBkYSBpaHIgZGVuIGZvbGdlbmRlbiBQcm96ZXNzIG5pY2h0IGplZGVzbWFsIG5ldSBkdXJjaGxhdWZlbiBtw7xzc3QsIGFsbGVyZGluZ3MgYXVjaCBldHdhcyBmZWhsZXJhbmbDpGxsaWcuIEZhbGxzIGVzIGVpbiBQcm9ibGVtIG1pdCBldXJlciBBdXRoZW50aWZpemllcnVuZyBnw61idCwgbcO8c3N0IGlociBkaWUgRGF0ZWkgIi5odHRyLW9hdXRoIiBow6RuZGlzY2ggbMO2c2NoZW4gdW5kIGRpZSBGdW5rdGlvbiBlcm5ldXQgYXVzZsO8aHJlbi4gSWNoIHfDvHJkZSBlbXBmZWhsZW4sIGhpZXIgIk5vIiBhdXN6dXfDpGhsZW4uDQoNCkRhbmFjaCDDtmZmbmV0IHNpY2ggZXVlciBCcm93c2VyIHVuZCBpaHIgbGFuZGV0IGF1ZiBlaW5lciBBdXRoZW50aWZpemllcnVuZ3NzZWl0ZS4gRG9ydCBtw7xzc3QgaWhyIGV1Y2ggKiptaXQgZGVtIEFjY291bnQgZWlubG9nZ2VuLCBkZW4gaWhyIGFscyBUZXN0IEFjY291bnQgaW4gZGVyIEFwcCBhbmdlZ2ViZW4gaGFidCoqLiBEYW5hY2ggc3RpbW10IGlociB6dSwgZGFzcyBpaHIgZGVyIEVudHdpY2tsZXJcKmluIChldWNoIHNlbGJzdCkgdmVydHJhdXQgdW5kIFp1Z3JpZmYgYXVmIGRpZSBBUEkgZ2V3w6RocnQuIFVuZCBmZXJ0aWchDQoNCmBgYHtyfQ0KeXRfb2F1dGgoDQogIGFwcF9pZCA9ICJ4eHh4eHh4eCIsDQogIGFwcF9zZWNyZXQgPSAieHh4eHh4eHgiKQ0KYGBgDQoNCiMjIFp1Z3JpZmYgYXVmIE1ldGFkYXRlbiB2b24gVmlkZW9zDQoNClRlc3RlbiB3aXIgenVuw6RjaHN0IGFuaGFuZCBlaW5lcyBlaW56ZWxuZW4gVmlkZW9zLCBvYiB1bnNlciBadWdyaWZmIGZ1bmt0aW9uaWVydC4gRGFzIGdlaHQgZWluZmFjaCBtaXQgZGVyIEZ1bmt0aW9uIGBnZXRfdmlkZW9fZGV0YWlscygpYCwgZGllIHVucyBmw7xyIGVpbmUgVmlkZW8gSUQgZGllIHNwZXppZml6aWVydGVuIE1ldGFkYXRlbiB6dXLDvGNrZ2lidC4gRGllIElEIGVpbmVzIFZpZGVvcyBmaW5kZXQgaWhyLCB3ZW5uIGlociBlaW4gVmlkZW8gYXVmIFlvdVR1YmUgYXVmcnVmdCwgaW4gZGVyIEFkcmVzc3plaWxlIGV1cmVzIEJyb3dzZXJzIG5hY2ggZGVtIFNjaGVtYTogaHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g/dj0qKlZJREVPX0lEKioNCg0KV2lyIHdvbGxlbiBiZWlzcGllbHN3ZWlzZSBmw7xyIGRpZSBSZWdpZXJ1bmdzZXJrbMOkcnVuZyB2b24gS2FuemxlciBTY2hvbHosIGRhcyBzb2dlbmFubnRlIGAic25pcHBldCJgLCBhbHNvIGRpZSBLdXJ6aW5mb3JtYXRpb25lbiB6dSBUaXRlbCwgUHVibGlrYXRpb25zZGF0dW0sIEJlc2NocmVpYnVuZyBldGMuIFdlaXRlcmUgSW5mb3JtYXRpb25lbiB6dSBkZW4gdmVyZsO8Z2JhcmVuIERhdGVuIGZpbmRldCBpaHIgdW50ZXIgYD9nZXRfdmlkZW9fZGV0YWlscygpYC4gQXXDn2VyZGVtIGvDtm5uZW4gd2lyIG1pdCBgYXMuZGF0YS5mcmFtZSA9IFRSVUVgIHNwZXppZml6aWVyZW4sIGRhc3Mgd2lyIGRpZSBEYXRlbiBnZXJuZSBhbHMgRGF0YWZyYW1lIGjDpHR0ZW4sIGJlaSBkZW0gZWluZSBTcGFsdGUgZWluZXIgVmFyaWFibGUgZW50c3ByaWNodC4NCg0KYGBge3J9DQp2aWRlb19kYXRhIDwtIGdldF92aWRlb19kZXRhaWxzKHZpZGVvX2lkID0gIjZxU1ZNLUFQYXVvIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcGFydCA9ICJzbmlwcGV0IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYXMuZGF0YS5mcmFtZSA9IFRSVUUpDQpgYGANCg0KQXVmIGRpZXNlbGJlIEFydCB1bmQgV2Vpc2Uga8O2bm5lbiB3aXIgYXVjaCBmw7xyIG1laHJlcmUgVmlkZW9zIGF1ZiBlaW5tYWwgRGF0ZW4gemllaGVuLCBpbmRlbSB3aXIgZWluZW4gVmVjdG9yIGF1cyBkZW4gSURzIG1hY2hlbi4gRGFubiBpc3QgamVkZSBSZWloZSBpbSBEYXRhZnJhbWUgZWluIFZpZGVvLg0KDQpgYGB7cn0NCnZpZGVvX2RhdGEgPC0gZ2V0X3ZpZGVvX2RldGFpbHModmlkZW9faWQgPSBjKCI2cVNWTS1BUGF1byIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIlE1ejlRTHJTTGZRIiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiUXl6WnZ6RDhnRDQiKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcGFydCA9ICJzbmlwcGV0IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYXMuZGF0YS5mcmFtZSA9IFRSVUUpDQpgYGANCg0KIyMgWnVncmlmZiBhdWYgS29tbWVudGFyZQ0KDQohW10oY29tbWVudHMucG5nKQ0KDQpVbSBhdWYgS29tbWVudGFyZSBmw7xyIGVpbnplbG5lIFZpZGVvcyB6dXp1Z3JlaWZlbiwga8O2bm5lbiB3aXIgZGllIEZ1bmt0aW9uIGBnZXRfYWxsX2NvbW1lbnRzKClgIHZlcndlbmRlbiwgd2VsY2hlIGxlZGlnbGljaCBkaWUgVmlkZW8gSUQgYmVuw7Z0aWd0LiBCZWFjaHRldCwgZGFzcyBgZ2V0X2FsbF9jb21tZW50cygpYCBhbGxlcmRpbmdzIG5pY2h0IGFsbGUgQW50d29ydGVuIGF1ZiBLb21tZW50YXJlIGF1c2dpYnQuDQoNCmBgYHtyfQ0KY29tbWVudHMgPC0gZ2V0X2FsbF9jb21tZW50cyh2aWRlb19pZCA9ICI2cVNWTS1BUGF1byIpDQpgYGANCg0KYGdldF9hbGxfY29tbWVudHMoKWAga2FubiBqZWRvY2ggaW1tZXIgbnVyIGRpZSBLb21tZW50YXJlIGbDvHIgZWluIFZpZGVvIGF1ZiBlaW5tYWwgemllaGVuLiBXZW5uIHdpciBkaWUgS29tbWVudGFyZSBmw7xyIG1laHJlcmUgVmlkZW9zIHdvbGxlbiwga8O2bm5lbiB3aXIgZGllcyBlbnR3ZWRlciBlcnJlaWNoZW4sIGluZGVtIHdpciBkaWUgRnVua3Rpb24gZsO8ciBqZWRlIFZpZGVvIElEIGVpbnplbG4gYXVzZsO8aHJlbi4gRWxlZ2FudGVyIGlzdCBlcyBqZWRvY2gsIGVpbmVuICoqZm9yLWxvb3AqKiB6dSB2ZXJ3ZW5kZW4uIERpZXNlciBlcmxhdWJ0IGVzLCBpbiBlaW5lciAiU2NobGVpZmUiIChlaW5lbSBMb29wKSBkdXJjaCBhbGxlIFZpZGVvcyBkdXJjaHp1Z2VoZW4gdW5kIGRpZSBLb21tZW50YXJlIGRlciBWaWRlb3MgamV3ZWlscyBtaXQgYGdldF9hbGxfY29tbWVudHMoKWAgenUgemllaGVuLiBXaXIgZ2VoZW4gYWxzbyB6d2lzY2hlbiBSZWloZSBgMWAgdW5kIGRlciBHZXNhbXR6YWhsIGRlciBSZWloZW4gaW0gRGF0ZW5zYXR6IChgbnJvdyh2aWRlb19kYXRhKWApIGR1cmNoIGplZGVuIEVpbnRyYWcgdW5kIGbDvGxsZW4gaW4gZGVuIGZvbGdlbmRlbiBPcGVyYXRpb25lbiB1bmQgRnVua3Rpb25lbiBkZW4gSW5kaWthdG9yIGBbaV1gIGVpbiwgd2VsY2hlIGplIGRpZSBha3R1ZWxsZSBSZWloZSBhbmdpYnQuIERpZXNlIGVpbnplbG5lbiBEYXRhZnJhbWVzLCBkaWUgc28gaW4gamVkZXIgUnVuZGUgZGVzIExvb3BzIGVyc3RlbGx0IHdlcmRlbiwgd2VyZGVuIGRhbm4gZWluZW0gKGFuZmFuZ3MgbGVlcmVuKSBEYXRhZnJhbWUgYnp3LiBUaWJibGUgcnVuZGVud2Vpc2UgaGluenVnZWbDvGd0LiBBbSBFbmRlIGRlcyBMb29wcyBiYXVlbiB3aXIgbWl0IGBTeXMuc2xlZXAoKWAgZWluZSBrbGVpbmUgUGF1c2UgZWluLiBTbyAic2NobMOkZnQiIGRhcyBTeXN0ZW0gamUgZWluZSBTZWt1bmRlLCBiZXZvciBlcyBtaXQgZGVtIG7DpGNoc3RlbiBLYW5hbCB3ZWl0ZXJnZWh0LiBEaWVzZSBQYXVzZW4gYmV1Z2VuIEZlaGxlcm1lbGR1bmdlbiB2b24gQVBJcyB2b3IsIGRpZSBhdWZ0cmV0ZW4ga8O2bm5lbiwgd2VubiB3aXIgenUgc2NobmVsbCB6dSB2aWVsZSBBbmZyYWdlbiBzdGVsbGVuLiBJbiBkaWVzZW0gQmVpc3BpZWwgd29sbGVuIGRpZSBLb21tZW50YXJlIHZvbiBhbGxlbiBkcmVpIFZpZGVvcyBlcmhhbHRlbiwgZsO8ciBkaWUgd2lyIGJlcmVpdHMgZGllIE1ldGFkYXRlbiBnZXpvZ2VuIGhhYmVuLg0KDQpgYGB7cn0NCnZpZGVvX2NvbW1lbnRzIDwtIHRpYmJsZSgpICMgbGVlcmVyIENvbnRhaW5lcg0KDQpmb3IgKGkgaW4gMTpucm93KHZpZGVvX2RhdGEpKSB7DQogIA0KICBjb21tZW50c19wZXJfdmlkZW8gPC0gZ2V0X2FsbF9jb21tZW50cyh2aWRlb19kYXRhJGlkW2ldKSAjIEFQSSBDYWxsDQogIA0KICB2aWRlb19jb21tZW50cyA8LSAgYmluZF9yb3dzKHZpZGVvX2NvbW1lbnRzLCBjb21tZW50c19wZXJfdmlkZW8pICMgenVzYW1tZW5mw7xocmVuDQogIA0KICBTeXMuc2xlZXAoMSkgIyBQYXVzaWVyZW4NCn0NCmBgYA0KDQpXaXIga8O2bm5lbiBnZW5hdXNvIGd1dCBkdXJjaCBlaW5lbiBWZWN0b3IgbG9vcGVuLiBJbiBkaWVzZW0gRmFsbGUga8O2bm5lbiB3aXIgc3RhdHQgYG5yb3coKWAgZGllIEZ1bmt0aW9uIGBsZW5ndGgoKWAgdmVyd2VuZGVuIChlaW4gVmVrdG9yIGhhdCBrZWluZSBSZWloZW4sIGFiZXIgZWluZSBMw6RuZ2UsIGRpZSBkdXJjaCBkaWUgQW56YWhsIGRlciBlaW56ZWxuZW4gRWxlbWVudGUgYmVzdGltbXQgd2lyZCkuDQoNCmBgYHtyfQ0KaWRzIDwtIGMoIjZxU1ZNLUFQYXVvIiwgIyBWZWt0b3IgbWl0IElEcw0KICAgICAgICAgIlE1ejlRTHJTTGZRIiwNCiAgICAgICAgICJReXpadnpEOGdENCIpDQoNCnZpZGVvX2NvbW1lbnRzIDwtIHRpYmJsZSgpICMgbGVlcmVyIENvbnRhaW5lcg0KDQpmb3IgKGkgaW4gMTpsZW5ndGgoaWRzKSkgew0KICANCiAgY29tbWVudHNfcGVyX3ZpZGVvIDwtIGdldF9hbGxfY29tbWVudHMoaWRzW2ldKSAjIEFQSSBDYWxsDQogIA0KICB2aWRlb19jb21tZW50cyA8LSAgYmluZF9yb3dzKHZpZGVvX2NvbW1lbnRzLCBjb21tZW50c19wZXJfdmlkZW8pICMgenVzYW1tZW5mw7xocmVuDQogIA0KICBTeXMuc2xlZXAoMSkgIyBQYXVzaWVyZW4NCn0NCmBgYA0KDQoqKkVpbiB3aWNodGlnZXIgSGlud2VpcyB6dSBmb3ItbG9vcHMgaW4gKlIqOioqIGltIEdlZ2Vuc2F0eiB6dSAqUHl0aG9uKiwgZGFzIHZpZWxlIGRpZXNlciB1bmQgw6RobmxpY2hlciBMb29wcyB2ZXJ3ZW5kZXQsIHNpbmQgTG9vcHMgaW4gKlIqIG51ciBzZWx0ZW4gZWluZSBndXRlIElkZWUuIERhcyBsaWVndCBkYXJhbiwgZGFzcyBzaWUsIGluc2Jlc29uZGVyZSBmw7xyIGdyb8OfZSBEYXRlbnPDpHR6ZSwgc2NobGVjaHQgc2thbGllcmVuIHVuZCBzb21pdCBzY2huZWxsIHNlaHIgbGFuZ3NhbSB1bmQgaW5lZmZpemllbnQgd2VyZGVuLiBEYWhlciBzb2xsdGVuIHdpciBpbiAqUiogd2FubiBpbW1lciBtw7ZnbGljaCBGdW5rdGlvbmVuIHZlcnduZGVuLCBkaWUgZGlyZWt0IGF1ZiBkZW0gRGF0ZW5zYXR6IG9wZXJpZXJlbiwgZXR3YSBhdXMgZGVtICpUaWR5dmVyc2UqLiBEaWVzIG1hY2h0IGVzIGVpbmZhY2gsIGZvci1sb29wcyB6dSB2ZXJtZWlkZW4uIERlbm5vY2ggZ2lidCBlcyBlaW5lIGJlZ3Jlbnp0ZSBBbnphaGwgYW4gRsOkbGxlbiwgaW4gZGVuZW4gZm9yLWxvb3BzIHNpbm52b2xsIHNpbmQuIERpZXMgaXN0IGluc2Jlc29uZGVyZSBkZXIgRmFsbCBiZWkgQVBJIENhbGxzLCBkYSB3aXIgYnNwd3MuIG1pdCBgU3lzLnNsZWVwKClgIGtvbnRyb2xsaWVyZW4ga8O2bm5lbiwgd2lldmllbCBaZWl0IHp3aXNjaGVuIGRlbiBlaW56ZWxuZW4gQW5mcmFnZW4gdmVyZ2VodC4gV2VubiB3aXIgYWxsZSBBbmZyYWdlbiBhdWYgZWlubWFsIHN0ZWxsZW4sIGthbm4gZXMgc2NobmVsbCBwYXNzaWVyZW4sIGRhc3MgdW5zZXIgWnVncmlmZiB0ZW1wb3LDpHIgZ2VzcGVycnQgd2lyZCwgdW0gZGllIEFQSSB2b3Igw5xiZXJsYXN0dW5nIHp1IHNjaMO8dHplbi4gRGVzaGFsYiBpc3QgZXMgb2Z0IHNpbm52b2xsLCBkaWVzZSBBcnQgdm9uIE9wZXJhdGlvbmVuIG5hY2hlaW5hbmRlciwgZWJlbiBpbiBlaW5lbSBMb29wLCBhdXN6dWbDvGhyZW4uDQoNCiMjIFZvcmdlc2NobGFnZW5lIFZpZGVvcw0KDQohW10ocmVsYXRlZF92aWRlb3MucG5nKQ0KDQpEaWUgRnVua3Rpb24gYGdldF9yZWxhdGVkX3ZpZGVvcygpYCBnaWJ0IHVucyBkaWUgdm9yZ2VzY2hsYWdlbmVuIFZpZGVvcyB6dSBlaW5lbSBWaWRlbyBhdXMuIE1pdCBgbWF4X3Jlc3VsdHNgIGdlYmVuIHdpciBkaWUgQW56YWhsIGFuIHZvcmdlc2NobGFnZW5lbiBWaWRlb3MgYW4sIGRpZSB3aXIgcHJvIFZpZGVvIGVyaGFsdGVuIG3DtmNodGVuIC0gYmlzIHp1IDUwIFN0w7xjay4gRXMgaXN0IGFsbGVyZGluZ3MgYW56dW5laG1lbiwgZGFzcyBudXIgd2VuaWdlciBVc2VyXCppbm5lbiBtZWhyIGFscyAxMCB2b3JnZXNjaGxhZ2VuZSBWaWRlb3Mgw7xiZXJoYXVwdCBzZWhlbi4gV2VyIHNjcm9sbHQgc2Nob24gbGFuZ2UgZHVyY2ggZGllc2UgTGlzdGU/IEF1w59lcmRlbSBiZWxhc3RldCBlcyB1bnNlciBBUEkgUmF0ZWxpbWl0IHJlbGF0aXYgc3RhcmssIGRpZSBEYXRlbiBkaWVzZXIgdm9nZXNjaGxhZ2VuZW4gVmlkZW9zIHp1IHppZWhlbiAobWVociBkYXp1IGluIGRlciBBUEkgRG9rdW1lbnRhdGlvbikuIERlc2hhbGIgZW50c2NoZWlkZW4gd2lyIHVucywgbnVyIDEwIHZvcmdlc2NobGFnZW5lIFZpZGVvcyB6dSB6aWVoZW46DQoNCmBgYHtyfQ0KcmVsYXRlZF92aWRlb3MgPC0gZ2V0X3JlbGF0ZWRfdmlkZW9zKCI2cVNWTS1BUGF1byIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWF4X3Jlc3VsdHMgPSAxMCkNCmBgYA0KDQpXZW5uIHdpciBkaWUgdm9yZ2VzY2hsYWdlbmVuIFZpZGVvcyBmw7xyIG1laHIgYWxzIGVpbiBWaWRlbyB6aWVoZW4gd29sbGVuLCBtw7xzc2VuIHdpciBlcm5ldXQgZWluZW4gZm9yLWxvb3AgdmVyd2VuZGVuLiBEaWUgVmlkZW8gSUQgZ2lidCBkYWJlaSBhbiwgd2VsY2hlcyBkYXMgIm9yaWdpbmFsZSIgVmlkZW8gaXN0LCB2b24gZGVtIHdpciBhdXNnZWdhbmdlbiBzaW5kLiBEYSBgZ2V0X3JlbGF0ZWRfdmlkZW9zKClgIHVucyBhdXRvbWF0aXNjaCBlaW5lbiBrdXJ6ZW4gVGV4dCBkYXp1IGF1c2dpYnQsIHdpZXZpZWxlIFZpZGVvcyBnZWZ1bmRlbiB3dXJkZW4sIGJlbnV0emVuIHdpciBkaWVzbWFsIGF1w59lcmRlbSBgY2F0KClgIHVtIGluIGplZGVyIFNjaGxlaWZlIGViZW5mYWxscyBlaW5lbiBrbGVpbmVuIFByaW50b3V0IGhpbnp1enVmw7xnZW4uIERpZXNlciBiZXN0ZWh0IGF1cyBkZXIgSUQgZGVzIGFrdHVlbGxlbiBWaWRlb3MgdW5kIGVpbmVtIEFic2F0ei4gU29taXQga8O2bm5lbiB3aXIgaW1tZXIgc2VoZW4sIHp1IHdlbGNoZW0gVmlkZW8gd2lldmllbGUgRXJnZWJuaXNzZSBnZWZ1bmRlbiB3dXJkZW4uDQoNCmBgYHtyfQ0KDQppZHMgPC0gYygiNnFTVk0tQVBhdW8iLCAjIFZla3RvciBtaXQgSURzDQogICAgICAgICAiUTV6OVFMclNMZlEiLA0KICAgICAgICAgIlF5elp2ekQ4Z0Q0IikNCg0KbW9yZV9yZWxhdGVkX3ZpZGVvcyA8LSB0aWJibGUoKQ0KDQpmb3IgKGkgaW4gMTpsZW5ndGgoaWRzKSkgew0KICANCiAgY2F0KGlkc1tpXSwgIlxuIikNCiAgDQogIHRyeSh7DQogICAgDQogICAgcmVsYXRlZF92aWRlb3NfcGVyX3ZpZGVvIDwtIGdldF9yZWxhdGVkX3ZpZGVvcyhpZHNbaV0sIG1heF9yZXN1bHRzID0gMTApIA0KICAgIA0KICAgIG1vcmVfcmVsYXRlZF92aWRlb3MgPC0gYmluZF9yb3dzKG1vcmVfcmVsYXRlZF92aWRlb3MsIHJlbGF0ZWRfdmlkZW9zX3Blcl92aWRlbykNCiAgICANCiAgICBTeXMuc2xlZXAoMSkNCiAgICANCiAgfSkNCiAgDQp9DQpgYGANCg0KIyMgWnVncmlmZiBhdWYgYWxsZSBWaWRlb3MgZWluZXMgS2FuYWxzDQoNCldvbGxlbiB3aXIgZGllIE1ldGFkYXRlbiBhbGxlciBWaWRlb3MgZWluZXMgQ2hhbm5lbHMgYXVmIGVpbm1hbCB6aWVoZW4sIGJyYXVjaGVuIHdpciB6dW7DpGNoc3QgZGllIENoYW5uZWwgSUQuIERpZXNlIGlzdCBsZWlkZXIgbmljaHQgZ2FueiBzbyBsZWljaHQgenUgZmluZGVuIHdpZSBkaWUgVmlkZW8gSUQuIFdlbm4gaWhyIG1pdCBkZW0gQnJvd3NlciBhdWYgZGllIFlvdVR1YmUgU2VpdGUgZWluZXMgS2FuYWxzIGdlaHQsIG3DvHNzdCBpaHIgZXVjaCBkZW4gU2VpdGVucXVlbGx0ZXh0IGFuemVpZ2VuIGxhc3NlbiAobWl0IEZpcmVmb3ggYnNwd3MuIHZpYSBSZWNodHNrbGljaykuIERhcmF1ZmhpbiBzZWh0IGlociBkZW4gSFRNTC1Db2RlIGRlciBTZWl0ZS4gRGFyaW4gdmVyc3RlY2t0IHNpY2ggZGllIENoYW5uZWwgSUQ6IHN1Y2h0IG1pdCBgc3RyK2ZgIG5hY2ggIipjaGFubmVsX2lkPSoiLiBEYXJhdWZoaW4gc29sbHRldCBpaHIgZWluZW4gQWJzY2huaXR0IGltIENvZGUgZW50ZGVja2VuLCBkZXIgZXVjaCBkaWUgQ2hhbm5lbCBJRCB2ZXJyw6R0LCBic3B3cy4gKmNoYW5uZWxfaWQ9VUM3VEFBMldZbFBmYjZlREpDZVg0dTB3KiBmw7xyIGRpZSBHcsO8bmVuLg0KDQpEaWVzZSBDaGFubmVsIElEIGvDtm5uZW4gd2lyIGRhbm4gbWl0IGRlciBGdW5rdGlvbiBgbGlzdF9jaGFubmVsX3ZpZGVvcygpYCBhbGxlIFZpZGVvcyBlaW5lcyBDaGFubmVscyBhdXMsIGJpcyB6dSBkZXIgbWF4aW1hbGVuIEFuemFobCBkaWUgd2lyIG1pdCBgbWF4X3Jlc3VsdHNgIGZlc3RsZWdlbi4gV2lyIGvDtm5uZW4gYEluZmAgKGbDvHIgSW5maW5pdGUsIGFsc28gdW5lbmRsaWNoKSBhbmdlYmVuLCB1bSBhbGxlIEVyZ2Vibmlzc2UgenUgZXJoYWx0ZW4uDQoNCmBgYHtyfQ0KZ3J1ZW5lX3ZpZGVvcyA8LSBsaXN0X2NoYW5uZWxfdmlkZW9zKGNoYW5uZWxfaWQgPSAiVUM3VEFBMldZbFBmYjZlREpDZVg0dTB3IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtYXhfcmVzdWx0cyA9IEluZikNCmBgYA0KDQpXaXIgYmVrb21tZW4gZGllIFZpZGVvcyBlcm5ldXQgaW4gZWluZW0gcHJha3Rpc2NoZW4gRGF0YWZyYW1lLCB3ZW5uIGF1Y2ggbWl0IGV0d2FzIHdlbmlnZXIgSW5mb3JtYXRpb25lbiBhbHMgdm9yaGVyLiBEYSB3aXIgamVkb2NoIGRpZSB2aWRlbyBJRHMgYWxsZXIgVmlkZW9zIGVyaGFsdGVuIGhhYmVuLCBrw7ZubnRlbiB3aXIgZGllc2UgZXJuZXV0IGFuIGBnZXRfdmlkZW9fZGV0YWlscygpYCBnZWJlbiwgdW0gbWVociBJbmZvcm1hdGlvbmVuIHp1IGVyaGFsdGVuLiBJY2ggbWFjaGUgZGllcyBlaW5tYWwgdGVzdHdlaXNlIGbDvHIgZGllIGVyc3RlbiA1IEVpbnRyw6RnZSBpbiBkZXIgU3BhbHRlICpjb250ZW50RGV0YWlscy52aWRlb0lEKiBhdXMgdW5zZXJlbSBEYXRhZnJhbWUgbWl0IGdyw7xuZW4gVmlkZW8uIERhenUgdmVyd2VuZGUgaWNoIGVpbmUgc29nZW5hbm50ZW4gKipJbmRleCoqLCB3ZWxjaGVyIHp3aXNjaGVuIGRlbiBlY2tpZ2VuIEtsYW1tZXJuIHN0ZWh0OiBgWzE6NV1gIHfDpGhsdCBkaWUgRWludHLDpGdlIDEgYmlzIDUgYXVzLCBgWzVdYCBkZW4gZsO8bmZ0ZW4gRWludHJhZywgZWluIHdlZ2xhc3NlbiBkZXMgSW5kZXggd8OkaGx0IGFsbGUgRWludHLDpGdlIGF1cy4gYCRgIHNwZXppZml6aWVydCBkaWUgU3BhbHRlIGVpbmVzIERhdGFmcmFtZXMuDQoNCmBgYHtyfQ0Kc29tZV92aWRlb19kYXRhIDwtIGdldF92aWRlb19kZXRhaWxzKHZpZGVvX2lkID0gZ3J1ZW5lX3ZpZGVvcyRjb250ZW50RGV0YWlscy52aWRlb0lkWzE6NV0sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcGFydCA9ICJzbmlwcGV0IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBhcy5kYXRhLmZyYW1lID0gVFJVRSkNCmBgYA0KDQpVbSBlaW5lbiDDnGJlcmJsaWNrIMO8YmVyIGRpZSB2ZXLDtmZmZW50bGljaHRlbiBWaWRlb3MgenUgZXJoYWx0ZW4sIGvDtm5uZW4gd2lyIG51biB3aWVkZXIgZGFzICpUaWR5dmVyc2UqIHVuZCAqZ2dwbG90KiB2ZXJ3ZW5kZW4sIHVtIERhdGVuIHp1IG1hbmlwdWxpZXJlbiB1bmQgenUgdmlzdWxpc2llcmVuLiBFcyBnaWJ0IGplZG9jaCBlaW4gUHJvYmxlbTogRGllIFZhcmlhYmFsZSAqY29udGVudERldGFpbHMucHVibGlzaGVkQXQqLCBhbHNvIGRlciBQdWJsaWthdGlvbnN6ZWl0cHVua3QsIGlzdCBlaW5lIENoYXJhY3Rlci1WYXJpYWJsZSAoYWxzbyBlaW5mYWNoIFplaWNoZW4pLiBEYXMga2FubiBzcMOkdGVyIFByb2JsZW1lIG1hY2hlbiwgZGEgdmllbGUgRnVua3Rpb25lbiBlaW5lIFZhcmlhYmxlIGltIERhdHVtcy1Gb3JtYXQgKFBPU0lYY3QpIGVyd2FydGVuLiBNaXQgYG11dGF0ZSgpYCBrw7ZubmVuIHdpciBkYXMgYWxsZXJkaW5ncyBsZWljaHQgYmVoZWJlbi4gRGllICpsdWJyaWRhdGUqLUZ1bmt0aW9uIGB5bWRfaG1zKClgIHdhbmRlbHQgZGVuIENoYXJhY3RlciBTdHJpbmcgamVkZXIgUmVpaGUgaW4gZWluZW4gRGF0dW1zLVN0cmluZyBuYWNoIGRlbSBGb3JtYXQgKipZKiplYXItKipNKipvbnRoLSoqRCoqYXlcXyoqSCoqb3VyLSoqTSoqaW51dGUqKi1TKiplY29uZCB1bToNCg0KYGBge3J9DQpncnVlbmVfdmlkZW9zIDwtIGdydWVuZV92aWRlb3MgJT4lIA0KICBtdXRhdGUoY29udGVudERldGFpbHMudmlkZW9QdWJsaXNoZWRBdCA9IHltZF9obXMoY29udGVudERldGFpbHMudmlkZW9QdWJsaXNoZWRBdCkpDQpgYGANCg0KSmV0enQga8O2bm5lbiB3aXIgYmVpc3BpZWxzd2Vpc2UgZWluZmFjaCBkaWUgVmlkZW9zIMO8YmVyIFplaXQgcGxvdHRlbiwgaW5kZW0gd2lyIHNpZSB6dW7DpGNoc3QgbWl0IGBzdW1tYXJpc2UoKWB3w7ZjaGVudGxpY2ggYWtrdW11bGllcmVuOg0KDQpgYGB7cn0NCmdydWVuZV92aWRlb3MgJT4lIA0KICBtdXRhdGUod2VlayA9IGZsb29yX2RhdGUoY29udGVudERldGFpbHMudmlkZW9QdWJsaXNoZWRBdCwgdW5pdCA9ICJ3ZWVrIikpICU+JSANCiAgc3VtbWFyaXNlKHZpZGVvcyA9IG4oKSwgLmJ5ID0gd2VlaykgJT4lIA0KICBnZ3Bsb3QoYWVzKHggPSB3ZWVrLCB5ID0gdmlkZW9zKSkgKw0KICBnZW9tX2xpbmUoKQ0KYGBgDQoNCiMjIyANCg0KIyDDnGJ1bmdlbg0KDQoqKklociBzZWlkIGRyYW46KiogV2llZGVyaG9sdCBkaWUgb2JpZ2VuIFNjaHJpdHRlIGbDvHIgVmlkZW9zLCBkaWUgZsO8ciBldXJlIEZvcnNjaHVuZ3NmcmFnZSByZWxldmFudCBzaW5kLiBGYWxscyBpaHIgZGllIE1ldGFkYXRlbiBvZGVyIElEcyBhbGxlciBWaWRlb3MgbWVocmVyZXIgS2Fuw6RsZSB6aWVoZW4gd29sbHQsIGZpbmRldCBpaHIgdW50ZW4gQmVpc3BpZWxjb2RlLiBEb3J0IGZpbmRldCBpaHIgYXVjaCBIaW53ZWlzZSB1bmQgQ29kZSwgdW0gZGllIGluIGRlciBFaW5mw7xocnVuZ3NwcsOkc2VudGF0aW9uIHZvcmdlc3RlbGx0ZW4gRmFsbGJlaXNwaWVsZSB6dSByZXBsaXppZXJlbi4NCg0KIyMgMS4gTWV0YWRhdGVuDQoNClppZWh0IGV1Y2ggZGllIE1ldGFkYXRlbiBlaW5lcyBvZGVyIG1laHJlcmVyIFZpZGVvcy4NCg0KYGBge3J9DQoNCmBgYA0KDQojIyAyLiBLb21tZW50YXJlDQoNClppZWh0IGV1Y2ggZGllIEtvbW1lbnRhcmUgZsO8ciBkaWUgVmlkZW9zLCBkaWUgaWhyIGbDvHIgMS4gYmVyZWl0cyBuYWNoZ2VzY2hsYWdlbiBoYWJ0Lg0KDQpgYGB7cn0NCg0KYGBgDQoNCiMjIDMuIFZvcmdlc2NobGFnZW5lIFZpZGVvcw0KDQpaaWVodCBkaWUgZXJzdGVuIDEwIHZvcmdlc2NobGFnZW5lbiBWaWRlb3MgZsO8ciBldXJlIFZpZGVvcy4gSGlud2VpczogV2VubiBpaHIgc2VociB2aWVsZSBWaWRlb3MgbmFjaHNjaGxhZ3QsIGthbm4gZGllcyB6dW0gRXJyZWljaGVuIGRlcyBSYXRlbGltaXRzIGbDvGhyZW4uIFp1bSBVbWdhbmcgZGFtaXQgZmluZGV0IGlociBhbSBFbmRlIGRpZXNlcyBEb2t1bWVudHMgKEFic2NobmlpdCAiVm9yZ2VzY2hsYWdlbmUgVmlkZW9zIHp1IFBhcnRlaXZpZGVvcyIpIEluZm9ybWF0aW9uZW4uIEbDvHIgw5xidW5nc3p3ZWNrZSByZWljaHQgZXMsIHdlbm4gaWhyIGbDvHIgbWF4aW1hbCAxMCBWaWRlb3MgZGllIHZvcmdlc2NobGFnZW5lbiBWaWRlb3MgemllaHQuDQoNCmBgYHtyfQ0KDQpgYGANCg0KIyMgNC4gQW5hbHlzZQ0KDQpBbmFseXNpZXJ0IGV1cmUgRXJnZWJuaXNzZToNCg0KMS4gIExhc3N0IGV1Y2ggZGllIEFuemFobCBhbiBLb21tZW50YXJlIHBybyBWaWRlbyBtaXQgYHN1bW1hcmlzZSgpYCB6dXNhbW1lbmZhc3Nlbg0KMi4gIFZpc3VhbGlzaWVydCBkaWUgTWVuZ2UgYW4gS29tbWVudGFyZW4gw7xiZXIgWmVpdCAoZ2dmLiBwcm8gVmlkZW8pDQozLiAgTGFzc3QgZXVjaCBkYXMgZHVyc2Nobml0dGxpY2hlIFB1Ymxpa2F0aW9uc2RhdHVtIGRlciB2b3JnZXNjaGxhZ2VuZW4gVmlkZW9zIHBybyBBdXNnYW5nc3ZpZGVvIGF1c2dlYmVuDQoNCmBgYHtyfQ0KDQpgYGANCg0KIyBSZXBsaWthdGlvbiBkZXIgRmFsbGJlaXNwaWVsZQ0KDQojIyBGYWxsYmVpc3BpZWw6IFlvdXR1YmUgS2Fuw6RsZSBkZXIgUGFydGVpZW4NCg0KRGFzIEZhbGxiZWlzcGllbCBhdXMgZGVyIFByw6RzZW50YXRpb24gc2luZCBkaWUgWW91VHViZSBLYW7DpGxlIGRlciBncm/Dn2VuIGRldXRzY2hlbiBQYXJ0ZWllbiAuIEbDvHIgZXVyZSBlaWdlbmVuIFByb2pla3RlIGvDtm5udCBpaHIgbmF0w7xybGljaCBhbGxlIGFuZGVyZW4gS2Fuw6RsZSB2ZXJ3ZW5kZW4uIEluIGRpZXNlbSBBYnNjaG5pdHQgZmluZGV0IGlociBDb2RlLCB1bSBkYXMgQmVpc3BpZWwgenUgcmVwcm9kdXppZXJlbiBvZGVyIChtaXQgbGVpY2h0ZW4gw4RuZGVydW5nZW4pIGF1ZiBhbmRlcmUgS2Fuw6RsZSBhbnp1d2VuZGVuLg0KDQohW10ocGFydHlfY2hhbm5lbHMucG5nKQ0KDQpXZW5uIHdpciBkaWUgVmlkZW9zIG1laHJlcmVyIEthbsOkbGUgemllaGVuIHdvbGxlbiwgc29sbHRlbiB3aXIgenVuw6RjaHN0IGRpZSBDaGFubmVsIElEcyAoaMOkbmRpc2NoKSBoZXJhdXNzdWNoZW4gdW5kIGFibGVnZW4uIERhIGljaCBhdcOfZXJkZW0gdmVybWVya2VuIHdpbGwsIHdlbGNoZSBQYXJ0ZWkgenUgd2VsY2hlbSBDaGFubmVsIGdlaMO2cnQsIGxlZ2UgaWNoIGRhenUgZWluZSB0aWJibGUsIGFsc28gZWluZSBBcnQgRGF0YWZyYW1lLCBhbjoNCg0KYGBge3J9DQpwYXJ0eV9jaGFubmVscyA8LSB0aWJibGUoDQogIHBhcnR5ID0gYygiY2R1IiwNCiAgICAgICAgICAgICJzcGQiLA0KICAgICAgICAgICAgImdydWVuZSIsDQogICAgICAgICAgICAiZmRwIiwgDQogICAgICAgICAgICAiYWZkIiwgDQogICAgICAgICAgICAibGlua2UiKSwNCiAgY2hhbm5lbF9pZCA9IGMoIlVDS3lXSUVzZTN1N0V4S2ZBV3VETVZudyIsDQogICAgICAgICAgICAgICAgICJVQ1NtYksxV3RwWW4yc09HTHZTU1hrS3ciLA0KICAgICAgICAgICAgICAgICAiVUM3VEFBMldZbFBmYjZlREpDZVg0dTB3IiwNCiAgICAgICAgICAgICAgICAgIlVDLXNNa3Jmb1FESC14ek14UE5ja0dGdyIsDQogICAgICAgICAgICAgICAgICJVQ3Eycm9nYXhMdFFGcllHM1gzS1lOd3ciLA0KICAgICAgICAgICAgICAgICAiVUNBOTVUNWJTR3hOT0FPREJkYlIycllRIikNCikNCmBgYA0KDQpVbSBqZXR6dCBhdWYgYWxsZSBLYW7DpGxlIHp1enVncmVpZmVuLCBrYW5uIGljaCBlaW5lbiBzb2dhbm50ZW4gKipmb3ItbG9vcCoqIHZlcndlbmRlbi4gRGllc2VyIGVybGF1YnQgZXMsIGluIGVpbmVyICJTY2hsZWlmZSIgKGVpbmVtIExvb3ApIGR1cmNoIGFsbGUgS2Fuw6RsZSBkdXJjaHp1Z2VoZW4gdW5kIGRpZSBNZXRhZGF0ZW4gZGVyIFZpZGVvcyBqZXdlaWxzIG1pdCBgbGlzdF9jaGFubmVsX3ZpZGVvcygpYCB6dSB6aWVoZW4uIFdpciBnZWhlbiBhbHNvIHp3aXNjaGVuIFJlaWhlIGAxYCB1bmQgZGVyIEdlc2FtdHphaGwgZGVyIFJlaWhlbiBpbSBEYXRlbnNhdHogKGBucm93KHBhcnR5X2NoYW5uZWxzKWApIGR1cmNoIGplZGVuIEVpbnRyYWcgdW5kIGbDvGxsZW4gaW4gZGVuIGZvbGdlbmRlbiBPcGVyYXRpb25lbiB1bmQgRnVua3Rpb25lbiBkZW4gSW5kaWthdG9yIGBbaV1gIGVpbiwgd2VsY2hlIGplIGRpZSBha3R1ZWxsZSBSZWloZSBhbmdpYnQuIFNvIGbDvGdlbiB3aXIgYXXDn2VyZGVtIGRlbiBQYXJ0ZWktSW5kaWthdG9yIGRlciBha3R1ZWxsZW4gUmVpaGUgenUgdW5zZXJlbSBFcmdlYm5pcyBoaW56dS4gRGllc2UgZWluemVsbmVuIERhdGFmcmFtZXMsIGRpZSBzbyBpbiBqZWRlciBSdW5kZSBkZXMgTG9vcHMgZXJzdGVsbHQgd2VyZGVuLCB3ZXJkZW4gZGFubiBlaW5lbSAoYW5mYW5ncyBsZWVyZW4pIERhdGFmcmFtZSBiencuIFRpYmJsZSBydW5kZW53ZWlzZSBoaW56dWdlZsO8Z3QuIEF1w59lcmRlbSBudXR6ZW4gd2lyIGBjYXQoKWAgZsO8ciBlaW5lbiBrbGVpbmVuIFByaW50b3V0LCBkZXIgdW5zIHZlcnLDpHQsIGbDvHIgd2VsY2hlIFBhcnRlaSB3aXIgd2llIHZpZWxlIFZpZGVvcyBlcmhhbHRlbi4gRGFzIGlzdCBhYmVyIG5pY2h0IHVuYmVkaW5ndCBuw7Z0aWcuIEFtIEVuZGUgZGVzIExvb3BzIGJhdWVuIHdpciBtaXQgYFN5cy5zbGVlcCgpYCBlaW5lIGtsZWluZSBQYXVzZSBlaW4uIFNvICJzY2hsw6RmdCIgZGFzIFN5c3RlbSBqZSBlaW5lIFNla3VuZGUsIGJldm9yIGVzIG1pdCBkZW0gbsOkY2hzdGVuIEthbmFsIHdlaXRlcmdlaHQuIERpZXNlIFBhdXNlbiBiZXVnZW4gRmVobGVybWVsZHVuZ2VuIHZvbiBBUElzIHZvciwgZGllIGF1ZnRyZXRlbiBrw7ZubmVuLCB3ZW5uIHdpciB6dSBzY2huZWxsIHp1IHZpZWxlIEFuZnJhZ2VuIHN0ZWxsZW4uDQoNCioqRWluIHdpY2h0aWdlciBIaW53ZWlzIHp1IGZvci1sb29wcyBpbiAqUio6KiogaW0gR2VnZW5zYXR6IHp1ICpQeXRob24qLCBkYXMgdmllbGUgZGllc2VyIHVuZCDDpGhubGljaGVyIExvb3BzIHZlcndlbmRldCwgc2luZCBMb29wcyBpbiAqUiogbnVyIHNlbHRlbiBlaW5lIGd1dGUgSWRlZS4gRGFzIGxpZWd0IGRhcmFuLCBkYXNzIHNpZSwgaW5zYmVzb25kZXJlIGbDvHIgZ3Jvw59lIERhdGVuc8OkdHplLCBzY2hsZWNodCBza2FsaWVyZW4gdW5kIHNvbWl0IHNjaG5lbGwgc2VociBsYW5nc2FtIHVuZCBpbmVmZml6aWVudCB3ZXJkZW4uIERhaGVyIHNvbGx0ZW4gd2lyIGluICpSKiB3YW5uIGltbWVyIG3DtmdsaWNoIEZ1bmt0aW9uZW4gdmVyd25kZW4sIGRpZSBkaXJla3QgYXVmIGRlbSBEYXRlbnNhdHogb3BlcmllcmVuLCBldHdhIGF1cyBkZW0gKlRpZHl2ZXJzZSouIERpZXMgbWFjaHQgZXMgZWluZmFjaCwgZm9yLWxvb3BzIHp1IHZlcm1laWRlbi4gRGVubm9jaCBnaWJ0IGVzIGVpbmUgYmVncmVuenRlIEFuemFobCBhbiBGw6RsbGVuLCBpbiBkZW5lbiBmb3ItbG9vcHMgc2lubnZvbGwgc2luZC4gRGllcyBpc3QgaW5zYmVzb25kZXJlIGRlciBGYWxsIGJlaSBBUEkgQ2FsbHMsIGRhIHdpciBic3B3cy4gbWl0IGBTeXMuc2xlZXAoKWAga29udHJvbGxpZXJlbiBrw7ZubmVuLCB3aWV2aWVsIFplaXQgendpc2NoZW4gZGVuIGVpbnplbG5lbiBBbmZyYWdlbiB2ZXJnZWh0LiBXZW5uIHdpciBhbGxlIEFuZnJhZ2VuIGF1ZiBlaW5tYWwgc3RlbGxlbiwga2FubiBlcyBzY2huZWxsIHBhc3NpZXJlbiwgZGFzcyB1bnNlciBadWdyaWZmIHRlbXBvcsOkciBnZXNwZXJydCB3aXJkLCB1bSBkaWUgQVBJIHZvciDDnGJlcmxhc3R1bmcgenUgc2Now7x0emVuLiBEZXNoYWxiIGlzdCBlcyBvZnQgc2lubnZvbGwsIGRpZXNlIEFydCB2b24gT3BlcmF0aW9uZW4gbmFjaGVpbmFuZGVyLCBlYmVuIGluIGVpbmVtIExvb3AsIGF1c3p1ZsO8aHJlbi4NCg0KYGBge3J9DQpwYXJ0eV92aWRlb3MgPC0gdGliYmxlKCkgIyBlbXB0eSBjb250YWluZXIgb2JqZWN0IHRvIGZpbGwNCg0KIyBsb29wIHRocm91Z2ggZXZlcnkgcm93IGluIHRoZSBkYXRhZnJhbWUgb2YgcGFydHkgY2hhbm5lbHMNCmZvciAoaSBpbiAxOm5yb3cocGFydHlfY2hhbm5lbHMpKSB7IA0KICAjIG1ha2UgdGhlIEFQSSBjYWxsDQogIHZpZGVvcyA8LSAgbGlzdF9jaGFubmVsX3ZpZGVvcygNCiAgICBjaGFubmVsX2lkID0gcGFydHlfY2hhbm5lbHMkY2hhbm5lbF9pZFtpXSwNCiAgICBtYXhfcmVzdWx0cyA9IEluZikNCiAgIyBhZGQgcGFydHkgaW5kaWNhdG9yDQogIHZpZGVvcyA8LSB2aWRlb3MgJT4lIG11dGF0ZShwYXJ0eSA9IHBhcnR5X2NoYW5uZWxzJHBhcnR5W2ldKSANCiAgIyBiaW5kIHJlc3VsdHMNCiAgcGFydHlfdmlkZW9zIDwtIGJpbmRfcm93cyhwYXJ0eV92aWRlb3MsIHZpZGVvcykgDQogICMgc29tZSBwcmludG91dCB0byBrZWVwIHRyYWNrDQogIGNhdChwYXJ0eV9jaGFubmVscyRwYXJ0eVtpXSwgIjogIiwgDQogICAgICBucm93KHZpZGVvcyksIA0KICAgICAgIiB2aWRlb3MgcmV0cmlldmVkIFxuIiwgDQogICAgICBzZXAgPSAiIikgDQogICMgYSBzbGVlcCBwZXJpb2QgYmV0d2VlbiBjYWxscyB0byBhdm9pZCBBUEkgZXJyb3JzDQogIFN5cy5zbGVlcCgxKSANCiAgDQp9DQoNCmBgYA0KDQpFcm5ldXQgd29sbGVuIHdpciBkaWUgVmFyaWFibGUgKmNvbnRlbnREZXRhaWxzLnZpZGVvUHVibGlzaGVkQXQqIGluIGVpbiByaWNodGlnZXMgRGF0dW0tRm9ybWF0IHVtd2FuZGVsbi4gQXXDn2VyZGVtIGvDtm5uZW4gd2lyIG1pdCBgZGlzdGluY3QoKWAgc2ljaGVyc3RlbGxlbiwgZGFzcyB3aXIga2VpbmUgRHVwbGlrYXRlIGluIHVuc2VyZW4gRGF0ZW4gaGFiZW4sIGluZGVtIHdpciBuYWNoIGVpbnppZ2FydGVuIFZpZGVvIElEcyBmaWx0ZXJuLiBBdcOfZXJkZW0gd29sbGVuIHdpciBub2NoIGVpbiAiTGFiZWwiIGbDvHIgZGllIFBhcnRlaWVuLCBhbHNvIGVpbmVuIFBhcnRlaWluZGlrYXRvciBmw7xyIGdyYWZpc2NoZSBEYXJzdGVsbHVuZ2VuIGV0Yy4sIGRlciBkZW4ga29tcGxldHRlbiAodW5kIGtvcnJla3QgZ2VzY2hyaWViZW5lbikgUGFydGVpbmFtZW4gZW50aMOkbHQuIERpZXNlcyAiZGF0YSBjbGVhbmluZyIgaXN0IGVpbiB3aWNodGlnZXIgQmVzdGFuZHRlaWwgZGVyIERhdGVudmVyYXJiZWl0dW5nLCBkZXIgZGFmw7xyIHNvcmd0LCBkYXNzIHdpciBmw7xyIHVuc2VyZSBBbmFseXNlbiBzYXViZXJlIERhdGVuIHp1ciBWZXJmw7xndW5nIGhhYmVuLiBOdXIgc28ga8O2bm5lbiB3aXIgZ3V0ZSBSZXN1bHRhdGUgZXJoYWx0ZW4hDQoNCmBgYHtyfQ0KcGFydHlfdmlkZW9zIDwtIHBhcnR5X3ZpZGVvcyAlPiUgDQogIG11dGF0ZShjb250ZW50RGV0YWlscy52aWRlb1B1Ymxpc2hlZEF0ID0geW1kX2htcyhjb250ZW50RGV0YWlscy52aWRlb1B1Ymxpc2hlZEF0KSkgJT4lICANCiAgZGlzdGluY3QoY29udGVudERldGFpbHMudmlkZW9JZCwgLmtlZXBfYWxsID0gVCkgJT4lIA0KICBtdXRhdGUocGFydHlfbGFiZWwgPSBjYXNlX3doZW4oIA0KICAgIHBhcnR5ID09ICJhZmQiIH4gIkFmRCIsDQogICAgcGFydHkgPT0gImNkdSIgfiAiQ0RVIiwNCiAgICBwYXJ0eSA9PSAic3BkIiB+ICJTUEQiLA0KICAgIHBhcnR5ID09ICJmZHAiIH4gIkZEUCIsDQogICAgcGFydHkgPT0gImdydWVuZSIgfiAiQsO8bmRuaXMgOTAvRGllIEdyw7xuZW4iLA0KICAgIHBhcnR5ID09ICJsaW5rZSIgfiAiRElFIExJTktFIg0KICApKQ0KYGBgDQoNCk5hY2hkZW0gd2lyIGRpZXNlIFNjaHJpdHRlIGR1cmNoZ2Vmw7xocnQgaGFiZW4sIGhhYmVuIHdpciBkZW5zZWxiZW4gKnBhcnR5X3ZpZGVvcyogRGF0ZW5zYXR6IHByb2R1emllcnQsIGRlbiBpaHIgYXVjaCBpbSBMZWFybndlYiBmaW5kZXQuIEluIGRlciB2b3JoZXJpZ2VuIMOcYnVuZyBoYWJ0IGlociBiZXJlaXRzIGRlbiBDb2RlIGtlbm5lbmdlbGVybnQsIGRlciB1bnMgZWluZSBow7xic2NoZSDDnGJlcnNpY2h0IMO8YmVyIGRpZSBQYXJ0ZWl2aWRlb3Mgw7xiZXIgWmVpdCBnZW5lcmllcnQ6DQoNCmBgYHtyfQ0KY29sb3JzIDwtDQogIGMoDQogICAgIkFmRCIgPSByZ2IoMCwgNjAsIDE0NSwgbWF4Q29sb3JWYWx1ZSA9IDI1NSksDQogICAgIkNEVSIgPSByZ2IoNTAsIDQ4LCA0NiwgbWF4Q29sb3JWYWx1ZSA9IDI1NSksDQogICAgIkRJRSBMSU5LRSIgPSByZ2IoMTgyLCAyOCwgNjIsIG1heENvbG9yVmFsdWUgPSAyNTUpLA0KICAgICJGRFAiID0gcmdiKDI1NSwgMjM3LCAwLCBtYXhDb2xvclZhbHVlID0gMjU1KSwNCiAgICAiQsO8bmRuaXMgOTAvRGllIEdyw7xuZW4iID0gcmdiKDcwLCAxNTAsIDQzLCBtYXhDb2xvclZhbHVlID0gMjU1KSwNCiAgICAiU1BEIiA9IHJnYigyMjcsIDAsIDE1LCBtYXhDb2xvclZhbHVlID0gMjU1KQ0KICApDQoNCnBhcnR5X3ZpZGVvcyAlPiUgDQogIG11dGF0ZShtb250aCA9IGZsb29yX2RhdGUoY29udGVudERldGFpbHMudmlkZW9QdWJsaXNoZWRBdCwgdW5pdCA9ICJtb250aCIpKSAlPiUgDQogIHN1bW1hcmlzZSh2aWRlb3MgPSBuKCksIC5ieSA9IGMobW9udGgsIHBhcnR5X2xhYmVsKSkgJT4lIA0KICBnZ3Bsb3QoYWVzKHggPSBtb250aCwgeSA9IHZpZGVvcywgY29sb3IgPSBwYXJ0eV9sYWJlbCkpICsNCiAgZ2VvbV9saW5lKCkgKw0KICBmYWNldF93cmFwKCB+IHBhcnR5X2xhYmVsLCBuY29sID0gMykgKw0KICBndWlkZXMoY29sb3IgPSAibm9uZSIpICsNCiAgc2NhbGVfY29sb3JfbWFudWFsKHZhbHVlcyA9IGNvbG9ycykgKw0KICBsYWJzKA0KICAgIHggPSAiTW9uYXQiLA0KICAgIHkgPSAiVmVyw7ZmZmVudGxpY2h0ZSBWaWRlb3MiLA0KICAgIGNvbG9yID0gIlBhcnRlaSIsDQogICAgdGl0bGUgPSAiVmlkZW9zIMO8YmVyIFplaXQgbmFjaCBQYXJ0ZWksIEdlc2FtdHplaXRyYXVtIg0KICApICsNCiAgdGhlbWVfYncoKQ0KYGBgDQoNCiMjIEtvbW1lbnRhcmUgenUgUGFydGVpdmlkZW9zDQoNCldlbm4gd2lyIG51biBkaWUgS29tbWVudGFyZSBkZXIgUGFydGVpdmlkZW9zIHppZWhlbiB3b2xsZW4sIHNvbGx0ZW4gd2lyIHp1ZXJzdCBkZW4gQmVvYmFjaHR1bmdzemVpdHJhdW0gZWluc2NocsOkbmtlbi4gQW5zb25zdGVuIGthbm4gZXMgbGVpY2h0IHBhc3NpZXJlbiwgZGFzcyB3aXIsIHdlbm4gd2lyIGbDvHIgYWxsZSAxMy41MTIgVmlkZW9zIE1ldGFkYXRlbiB6aWVoZW4sIGFuIGRhcyBSYXRlIExpbWl0IGRlciBBUEkgZ2VyYXRlbiB1bmQgZWluZW4gVGFnIHdhcnRlbiBtw7xzc2VuLCBiZXZvciB3aXIgd2VpdGVyYXJiZWl0ZW4ga8O2bm5lbiAoenVtIFVtZ2FuZyBkYW1pdCBzcMOkdGVyIG1laHIpLiBEZXNoYWxiIHdvbGxlbiB3aXIgdW5zIGV4ZW1wbGFyaXNjaCBhdWYgZGllIGltIEphbnVhciAyMDIyIHZlcsO2ZmZlbnRsaWNoZW4gVmlkZW9zIGJlc2NocsOkbmtlbi4gRGF6dSBzY2hyw6Rua2VuIHdpciB6dW7DpGNoc3QgbWl0IGBmaWx0ZXIoKWAgZGVuIERhdGVuc2F0eiBlaW46DQoNCmBgYHtyfQ0KdmlkZW9zX2phbnVhcnkgPC0gcGFydHlfdmlkZW9zICU+JSANCiAgZmlsdGVyKGNvbnRlbnREZXRhaWxzLnZpZGVvUHVibGlzaGVkQXQgPj0geW1kKCIyMDIyLTAxLTAxIikgJiANCiAgICAgICAgIGNvbnRlbnREZXRhaWxzLnZpZGVvUHVibGlzaGVkQXQgPCB5bWQoIjIwMjItMDItMDEiKSkgDQoNCmBgYA0KDQpEYXJhdWZoaW4gZ2VoZW4gd2lyIMOkaG5saWNoIHZvciB3aWUgZsO8ciBkaWUgS2Fuw6RsZSwgaW5kZW0gd2lyIGVpbmVuIGZvci1sb29wIHZlcndlbmRlbjoNCg0KYGBge3J9DQpjb21tZW50c19qYW51YXJ5IDwtIHRpYmJsZSgpICMgZW1wdHkgY29udGFpbmVyDQoNCmZvciAoaSBpbiB2aWRlb3NfamFudWFyeSRjb250ZW50RGV0YWlscy52aWRlb0lkKSB7IA0KICAjIG5vdGUgaG93IHdlIGNhbiBsb29wIGRpcmVjdGx5IG92ZXIgdGhlIGNvbnRlbnRzIG9mIHRoaXMgbGlzdCwgcmF0aGVyIHRoYW4gdXNpbmcgaSBpbiAxOm5yb3codmlkZW9zX2phbnVhcnkpIGFuZCBhY2Nlc3NpbmcgdGhlIGNvbnRlbnQgbGF0ZXIgdGhyb3VnaCB2aWRlb3NfamFudWFyeSRjb250ZW50RGV0YWlscy52aWRlb0lkW2ldDQogIA0KICBjb21tZW50cyA8LSBOVUxMICMgdGhpcyBpcyBvbmx5IG5lY2Vzc2FyeSB0byBpbmRpY2F0ZSBtaXNzaW5nIC8gZXJyb25lb3VzIEFQSSBjYWxscyBjb3JyZWN0bHkgKG90aGVyd2lzZSB3ZSBjb3VsZCBqdXN0IG92ZXJ3cml0ZSBpdCBldmVyeSBsb29wKQ0KICANCiAgY2F0KGksICJcbiIpDQogIA0KICANCiAgdHJ5KHsgICMgd3JhcHBpbmcgdGhlIGNhbGwgaW4gdHJ5KCkgbWFrZXMgc3VyZSB0aGUgbG9vcCBjb250aW51ZXMgd2hlbiBhbiBBUEkgY2FsbCBmYWlscy4gV3JhcHBpbmcgYmluZF9yb3dzKCkgYXMgd2VsbCBfYWZ0ZXJfIHRoZSBjYWxsIG1ha2VzIHN1cmUgcm93cyBhcmUgb25seSBib3VuZCBpZiBjb21tZW50cyBhcmUgcmV0cmlldmVkDQogICAgDQogICAgICBjb21tZW50cyA8LSBnZXRfYWxsX2NvbW1lbnRzKHZpZGVvX2lkID0gaSkgIyBtYWtlIEFQSSBjYWxsDQogICAgDQogICAgICBjb21tZW50c19qYW51YXJ5IDwtIGJpbmRfcm93cyhjb21tZW50c19qYW51YXJ5LCBjb21tZW50cykgIyBiaW5kIHJlc3VsdHMNCiAgICAgIA0KICB9KQ0KICANCiAgaWYoIWlzLm51bGwoY29tbWVudHMpKSB7Y2F0KG5yb3coY29tbWVudHMpLCAiY29tbWVudHMgcmV0cmlldmVkXG4iKX0gIyBzb21lIG91dHB1dCB0byBrZWVwIHRyYWNrDQogIGVsc2Uge2NhdCgibm8gcmVzdWx0cyBcbiIpfQ0KICAgIA0KICBTeXMuc2xlZXAoMSkgIyBhIHNsZWVwIHBlcmlvZCBiZXR3ZWVuIGNhbGxzIHRvIGF2b2lkIEFQSSBlcnJvcnMNCn0NCmBgYA0KDQpKZXR6dCBmZWhsdCB1bnMgYWxsZXJkaW5ncyBlaW4gSW5kaWthdG9yLCB3ZWxjaGVzIFZpZGVvICh1bmQgZGFtaXQgd2VsY2hlIEtvbW1lbnRhcmUpIHp1IHdlbGNoZXIgUGFydGVpIGdlaMO2cnQuIEVpbmVuIHNvbGNoZW4gSW5kaWthdG9yIGtvbm50ZW4gd2lyIGF1Y2ggbmljaHQgaW0gTG9vcCBzZWxic3QgZXJzdGVsbGVuLCBkYSB3aXIgZGllc21hbCBuaWNodCDDvGJlciBkaWUgUmVpaGVuLCBzb25kZXJuIMO8YmVyIGRpZSBXZXJ0ZSBlaW5lciBWYXJpYWJsZSBnZWxvb3B0IGhhYmVuLiBNaXQgZGVyIEZ1bmt0aW9uIGBsZWZ0X2pvaW4oKWAga8O2bm5lbiB3aXIgYWxsZXJkaW5ncyBlaW5mYWNoIGRpZSBXZXJ0ZSBhdXMgZWluZW0gYW5kZXJlbiBEYXRlbnNhdHogaGl6dWbDvGdlbi4gQmVpIGRpZXNlciBHZWxlZ2VuaGVpdCBuZWhtZW4gd2lyIGF1w59lcmRlbSBub2NoIGVpbiB3ZW5pZyBEYXRhY2xlYW5pbmcgdm9yOg0KDQpgYGB7cn0NCnBhcnR5X2NvbW1lbnRzX2phbnVhcnkgPC0gDQogIGxlZnRfam9pbih2aWRlb3NfamFudWFyeSwgDQogICAgICAgICAgICBjb21tZW50c19qYW51YXJ5ICU+JSAjIGpvaW4gdGhlIGNvbW1lbnRzIG9uIHRoZSB2aWRlbyBsaXN0Li4uDQogICAgICAgICAgICAgIHNlbGVjdCh2aWRlb0lkLCB0ZXh0RGlzcGxheSwgdGV4dE9yaWdpbmFsLCBhdXRob3JEaXNwbGF5TmFtZSwgDQogICAgICAgICAgICAgICAgICAgICBsaWtlQ291bnQsIHB1Ymxpc2hlZEF0LCBpZCkgJT4lICAjIC4uLiBidXQgc2VsZWN0IHJlbGV2YW50IGRhdGEgZmlyc3QuLiANCiAgICAgICAgICAgICAgcmVuYW1lKGNvbW1lbnRfaWQgPSBpZCksICAgICMgLi4uIGFuZCByZW5hbWUgdGhlIElEIHZhcmlhYmxlIGZvciBjbGFyaXR5DQogICAgICAgICAgICAjIHdlIG5lZWQgdG8gc3BlY2lmeSB0aGF0IHRoZSBJRHMgaGF2ZSBkaWZmZXJlbnQgY29sdW1uIG5hbWVzIGluIHRoZSB0d28gZGF0YXNldHMNCiAgICAgICAgICAgIGJ5ID0gam9pbl9ieShjb250ZW50RGV0YWlscy52aWRlb0lkID09IHZpZGVvSWQpLCANCiAgICAgICAgICAgIG11bHRpcGxlID0gImFsbCIpICU+JSAgIyB3ZSBleHBlY3QgbXVsdGlwbGUgbWF0Y2hlcyBmcm9tIHRoZSByaWdodC1oYW5kIHNpZGUgKGNvbW1lbnRzX2phbnVhcnkpIHRvIHRoZSBsZWZ0LWhhbmQgc2lkZSAodmlkZW9zX2phbnVhcnkpDQogIG11dGF0ZShsaWtlQ291bnQgPSBhcy5pbnRlZ2VyKGxpa2VDb3VudCksICAgICAgICAgICAgICAjIGNvdW50IGFzIGludGVnZXIsIG5vdCBjaGFyYWN0ZXINCiAgICAgICAgIHB1Ymxpc2hlZEF0ID0geW1kX2htcyhwdWJsaXNoZWRBdCkpICU+JSAgICAgICAjIHByb3BlciBkYXRldGVpbWUgZm9ybWF0DQogIHJlbmFtZSh2aWRlb19pZCA9IGNvbnRlbnREZXRhaWxzLnZpZGVvSWQsICAgICAgICAgICAgICAjIHJlbmFtZSBzb21lIHZhcmlhYmxlcyBmb3IgY2xhcml0eQ0KICAgICAgICAgdmlkZW9fUHVibGlzaGVkQXQgPSBjb250ZW50RGV0YWlscy52aWRlb1B1Ymxpc2hlZEF0LA0KICAgICAgICAgY29tbWVudF9QdWJsaXNoZWRBdCA9IHB1Ymxpc2hlZEF0KSAlPiUgDQogIHNlbGVjdCghYyhraW5kLCBldGFnLCBpZCkpICAgIyBkcm9wIHNvbWUgdW5uZWNlc3NhcnkgdmFyaWFibGVzDQpgYGANCg0KSmV0enQga8O2bm5lbiB3aXIgdW5zIG1pdCBldHdhcyB6dXPDpHR6bGljaGVtIERhdGF3cmFuZ2xpbmcgZWluZSDDnGJlcmJsaWNrc3N0YXRpc3RpayBhdXNnZWJlbiBsYXNzZW46DQoNCmBgYHtyfQ0KcGFydHlfY29tbWVudHNfamFudWFyeSAlPiUgZ3JvdXBfYnkocGFydHlfbGFiZWwpICU+JSANCiAgIyBjb3VudCBub24tbWlzc2luZyBhbmQgbWlzc2luZyBjb21tZW50cw0KICBjb3VudCghaXMubmEoY29tbWVudF9pZCksIC5kcm9wID0gRikgJT4lIA0KICAjIHdyYW5nbGUgaW50byB3aWRlIGZvcm1hdA0KICBwaXZvdF93aWRlcihuYW1lc19mcm9tID0gYCFpcy5uYShjb21tZW50X2lkKWAsIHZhbHVlc19mcm9tID0gbikgJT4lICAgDQogICAjIHJlbmFtZSB2YXJpYWJsZXMgZm9yIGNsYXJpdHkNCiAgcmVuYW1lKGNvbW1lbnRzID0gIlRSVUUiLCB2aWRlb3Nfd2l0aG91dF9jb21tZW50cyA9ICJGQUxTRSIpICU+JSAgDQogICMgcmVwbGFjZSBOQSB2YWx1ZXMgd2l0aCAwDQogIHJlcGxhY2VfbmEobGlzdChjb21tZW50cyA9IDAsIHZpZGVvc193aXRob3V0X2NvbW1lbnRzID0gMCkpICU+JSAgICAgICAgDQogIGxlZnRfam9pbih2aWRlb3NfamFudWFyeSAlPiUgZ3JvdXBfYnkocGFydHlfbGFiZWwpICU+JSBzdW1tYXJpc2UodG90YWxfdmlkZW9zID0gbigpKSwgYnkgPSAicGFydHlfbGFiZWwiKSAlPiUgICMgYWRkIHRvdGFsIG51bWJlciBvZiB2aWRlb3MgdGhyb3VnaCBhbm90aGVyIGRhdGFmcmFtZQ0KICBtdXRhdGUoYXZlcmFnZV9jb21tZW50c19wZXJfdmlkZW8gPSBjb21tZW50cyAvIHRvdGFsX3ZpZGVvcykgICAjIGdldCByZWxhdGl2ZSBjb21tZW50cw0KYGBgDQoNCiMjIFZvcmdlc2NobGFnZW5lIFZpZGVvcyB6dSBQYXJ0ZWl2aWRlb3MNCg0KVW0gZGllIHZvcmdlc2NobGFnZW5lbiBWaWRlb3MgenUgZGVuIFBhcnRlaXZpZGVvcyBhdXMgZGVtIEphbnVhciAyMDIyIHp1IGVyaGFsdGVuLCBrw7ZubmVuIHdpciBhbmFsb2cgenUgZGVtIEJlaXNwaWVsIG9iZW4gdm9yZ2VoZW4uIERhIHdpciBqZWRvY2ggcmVsYXRpdiB2aWVsZSBWaWRlb3MgbmFjaHNjaGF1ZW4gd29sbGVuLCBrw7ZubmVuIHdpciBhbiBkYXMgUmF0ZWxpbWl0IGRlciBBUEkgc3Rvw59lbi4gSW4gZGllc2VtIEZhbGwgd8O8cmRlbiB3aXIgZGFzIEVyZ2VibmlzIGFic3BlaWNoZXJuIHVuZCwgbmFjaGRlbSBkYXMgUmF0ZWxpbWl0IHp1csO8Y2tnZXNldHp0IHd1cmRlIChpZFIuIGFtIG7DpGNoc3RlbiBUYWcpLCBkaWUgdmVyYmxpZWJlbmVuIFZpZGVvcyBuYWNoc2NobGFnZW46DQoNCmBgYHtyfQ0KcmVsYXRlZF92aWRlb3NfamFudWFyeSA8LSB0aWJibGUoKQ0KDQpmb3IgKGlkIGluIHZpZGVvc19qYW51YXJ5JGNvbnRlbnREZXRhaWxzLnZpZGVvSWQpIHsNCiAgDQogIGNhdChpZCwgIlxuIikNCiAgDQogIHRyeSh7DQogICAgDQogICAgcmVsYXRlZF92aWRlb3MgPC0gZ2V0X3JlbGF0ZWRfdmlkZW9zKGlkLCBtYXhfcmVzdWx0cyA9IDEwKSAjIHRvcCAxMCAtIHdlIGNhbiBhc3N1bWUgbW9zdCBwZW9wbGUgd29uJ3Qgc2Nyb2xsIGZ1cnRoZXIsIGFuZCB0aGlzIGlzIGxlc3MgdGF4aW5nIG9uIHRoZSByYXRlIGxpbWl0cyB0aGFuIGUuZy4gNTANCiAgICANCiAgICByZWxhdGVkX3ZpZGVvc19qYW51YXJ5IDwtIGJpbmRfcm93cyhyZWxhdGVkX3ZpZGVvc19qYW51YXJ5LCByZWxhdGVkX3ZpZGVvcykNCiAgICANCiAgICBTeXMuc2xlZXAoMSkNCiAgICANCiAgfSkNCiAgDQp9DQoNCnNhdmVSRFMocmVsYXRlZF92aWRlb3NfamFudWFyeSwgInJlbGF0ZWRfdmlkZW9zX2phbnVhcnlfcHQxLlJEUyIpDQoNCiMgaWYgd2UgaGl0IGFuIEFQSSByYXRlIGxpbWl0LCB3ZSBoYXZlIHRvIHdhaXQgZm9yIGl0IHRvIHJlZnJlc2ggYW5kIHJlc3VtZSB0aGUgY29sbGVjdGlvbg0KDQpyZWxhdGVkX3ZpZGVvc19qYW51YXJ5IDwtIHJlYWRSRFMoInJlbGF0ZWRfdmlkZW9zX2phbnVhcnlfcHQxLlJEUyIpDQoNCm1pc3NpbmdfcmVsYXRlZF92aWRlb3MgPC0gdmlkZW9zX2phbnVhcnkgJT4lIGFudGlfam9pbihyZWxhdGVkX3ZpZGVvc19qYW51YXJ5LCBieSA9IGpvaW5fYnkoY29udGVudERldGFpbHMudmlkZW9JZCA9PSB2aWRlb19pZCkpICMgYW4gYW50aV9qb2luIGlzIGFuIGVhc3kgd2F5IHRvIHJldHVybiBhbGwgdmlkZW9zIHdpdGhvdXQgYSBtYXRjaCBpbiB0aGUgcmVsYXRlZF92aWRlbyBkYXRhDQoNCg0KZm9yIChpZCBpbiBtaXNzaW5nX3JlbGF0ZWRfdmlkZW9zJGNvbnRlbnREZXRhaWxzLnZpZGVvSWQpIHsNCiAgDQogIGNhdChpZCwgIlxuIikNCiAgDQogIHRyeSh7DQogICAgDQogICAgcmVsYXRlZF92aWRlb3MgPC0gZ2V0X3JlbGF0ZWRfdmlkZW9zKGlkLCBtYXhfcmVzdWx0cyA9IDEwKSAjIHRvcCAxMCAtIHdlIGNhbiBhc3N1bWUgbW9zdCBwZW9wbGUgd29uJ3Qgc2Nyb2xsIGZ1cnRoZXIsIGFuZCB0aGlzIGlzIGxlc3MgdGF4aW5nIG9uIHRoZSByYXRlIGxpbWl0cyB0aGFuIGUuZy4gNTANCiAgICANCiAgICByZWxhdGVkX3ZpZGVvc19qYW51YXJ5IDwtIGJpbmRfcm93cyhyZWxhdGVkX3ZpZGVvc19qYW51YXJ5LCByZWxhdGVkX3ZpZGVvcykNCiAgICANCiAgICBTeXMuc2xlZXAoMSkNCiAgICANCiAgfSkNCiAgDQp9DQoNCnNhdmVSRFMocmVsYXRlZF92aWRlb3NfamFudWFyeSwgInJlbGF0ZWRfdmlkZW9zX2phbnVhcnkuUkRTIikNCmBgYA0K